added settings page and enabled disabling registration and admin user management

This commit is contained in:
slynn1324 2021-02-01 16:15:01 -06:00
parent cc0a95f8f3
commit deaa8ded34
14 changed files with 658 additions and 16 deletions

View file

@ -95,6 +95,7 @@ There is trivial security on the web pages to allow for multiple user support.
- download icon > [Download by Yoyo from the Noun Project](https://thenounproject.com/term/download/2120379/)
- share icon > [Share by Тимур Минвалеев from the Noun Project](https://thenounproject.com/term/share/1058858/)
- done icon > [done by Viktor Ostrovsky from the Noun Project](https://thenounproject.com/term/done/587164/)
- settings icon > [setting by LUTFI GANI AL ACHMAD from the Noun Project](https://thenounproject.com/term/settings/3291880/)
## server

View file

@ -3,18 +3,51 @@ app.addSetter('aboutModal.open', (data) => {
});
app.addSetter('aboutModal.close', (data) => {
data.apiKey = null;
data.showApiKey = false;
data.aboutModal.active = false;
});
app.addSetter('about.showApiKey', async (data, apiKey) => {
data.apiKey = apiKey;
data.showApiKey = true;
});
async function showApiKey(){
let result = await fetch("/api/apikey");
let json = await result.json();
let apiKey = json.apiKey;
store.do('about.showApiKey', encodeURIComponent(apiKey));
}
app.addComponent('aboutModal', (store) => { return new Reef("#aboutModal", {
store: store,
template: (data) => {
let apiKeyElement = "";
if ( data.showApiKey ){
apiKeyElement = /*html*/`
<div>
<h2><strong>api key for ${data.user.name}:</strong></h2>
<input value="${data.apiKey}" style="width: 100%">
<br /><br />
</div>
`;
}
return /*html*/`
<div class="modal ${data.aboutModal.active ? 'is-active' : ''}">
<div class="modal-background" data-onclick="aboutModal.close"></div>
<div class="modal-content">
<div class="box" style="font-family: monospace;">
<h1><strong>tinypin</strong></h1>
<div class="level mb-0">
<div class="level-left">
<h1><strong>tinypin</strong></h1>
</div>
<div class="level-right">
<a data-onclick-x="showApiKey">show api key</a>
</div>
</div>
<div>
<a href="https://www.github.com">github.com/slynn1324/tinypin</a>
<br />
@ -22,6 +55,9 @@ app.addComponent('aboutModal', (store) => { return new Reef("#aboutModal", {
<br />
&nbsp;
</div>
${apiKeyElement}
<div>
<h2><strong>credits</strong></h2>
client
@ -57,6 +93,8 @@ app.addComponent('aboutModal', (store) => { return new Reef("#aboutModal", {
&nbsp;share icon &raquo; <a href="https://thenounproject.com/term/share/1058858/">Share by Тимур Минвалеев from the Noun Project</a>
<br />
&nbsp;done icon &raquo; <a href="https://thenounproject.com/term/done/587164/">done by Viktor Ostrovsky from the Noun Project</a>
<br />
&nbsp;settings icon &raquo; <a href="https://thenounproject.com/term/settings/3291880/">setting by LUTFI GANI AL ACHMAD from the Noun Project</a>
<br />
<br />

View file

@ -19,13 +19,14 @@ app.addComponent('navbar', (store) => { return new Reef("#navbar", {
store: store,
template: (data) => {
let boardName = "";
let hiddenBoardImage = '';
if ( data.board && data.board.hidden ){
hiddenBoardImage = '<img alt="(hidden)" style="width: 16px; height: 16px; vertical-align: middle; margin-top: 2px;" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDI0IDI0IiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPkFydGJvYXJkIDY8L3RpdGxlPjxwYXRoIGQ9Ik0xMiw2LjkyQTguNjUsOC42NSwwLDAsMSwxOS44NSwxMmE4LjYxLDguNjEsMCwwLDEtMTUuNywwQTguNjUsOC42NSwwLDAsMSwxMiw2LjkybTAtMkExMC42MiwxMC42MiwwLDAsMCwyLDEyYTEwLjYsMTAuNiwwLDAsMCwyMCwwQTEwLjYyLDEwLjYyLDAsMCwwLDEyLDQuOTJaIj48L3BhdGg+PHBhdGggZD0iTTE0LjIxLDExLjEyYTEuMzQsMS4zNCwwLDAsMS0xLjMzLTEuMzMsMS4zMSwxLjMxLDAsMCwxLC41Mi0xQTMuNDQsMy40NCwwLDAsMCwxMiw4LjQ2LDMuNTQsMy41NCwwLDEsMCwxNS41NCwxMmEzLjQ0LDMuNDQsMCwwLDAtLjI5LTEuNEExLjMxLDEuMzEsMCwwLDEsMTQuMjEsMTEuMTJaIj48L3BhdGg+PHBhdGggZD0iTTE5LDIwYTEsMSwwLDAsMS0uNzEtLjI5bC0xNC0xNEExLDEsMCwwLDEsNS43MSw0LjI5bDE0LDE0YTEsMSwwLDAsMSwwLDEuNDJBMSwxLDAsMCwxLDE5LDIwWiI+PC9wYXRoPjwvc3ZnPg==" />';
}
let boardName = "";
if ( data.board ){
boardName = /*html*/`
<span class="navbar-item">
@ -64,9 +65,15 @@ app.addComponent('navbar', (store) => { return new Reef("#navbar", {
<img style="24px; height:24px;" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDI0IDI0IiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPkFydGJvYXJkIDY8L3RpdGxlPjxwYXRoIGQ9Ik0xMiw2LjkyQTguNjUsOC42NSwwLDAsMSwxOS44NSwxMmE4LjYxLDguNjEsMCwwLDEtMTUuNywwQTguNjUsOC42NSwwLDAsMSwxMiw2LjkybTAtMkExMC42MiwxMC42MiwwLDAsMCwyLDEyYTEwLjYsMTAuNiwwLDAsMCwyMCwwQTEwLjYyLDEwLjYyLDAsMCwwLDEyLDQuOTJaIj48L3BhdGg+PHBhdGggZD0iTTE0LjIxLDExLjEyYTEuMzQsMS4zNCwwLDAsMS0xLjMzLTEuMzMsMS4zMSwxLjMxLDAsMCwxLC41Mi0xQTMuNDQsMy40NCwwLDAsMCwxMiw4LjQ2LDMuNTQsMy41NCwwLDEsMCwxNS41NCwxMmEzLjQ0LDMuNDQsMCwwLDAtLjI5LTEuNEExLjMxLDEuMzEsMCwwLDEsMTQuMjEsMTEuMTJaIj48L3BhdGg+PHBhdGggZD0iTTE5LDIwYTEsMSwwLDAsMS0uNzEtLjI5bC0xNC0xNEExLDEsMCwwLDEsNS43MSw0LjI5bDE0LDE0YTEsMSwwLDAsMSwwLDEuNDJBMSwxLDAsMCwxLDE5LDIwWiI+PC9wYXRoPjwvc3ZnPg==" />
</a>`;
}
let settingsItem = "";
if (data.user.admin == "y"){
settingsItem = `
<a class="navbar-item has-text-right" href="./settings">
<span>settings</span>
<img style="20px; height:20px;" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDY0IDY0IiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPnNldHRpbmc8L3RpdGxlPjxwYXRoIGQ9Ik01OS42MSwyMS44M2wtNS04LjY2YTEsMSwwLDAsMC0xLjIzLS40M0w0Ni41MSwxNS41QTIyLjExLDIyLjExLDAsMCwwLDM5LDExLjE1TDM4LDMuODZBMSwxLDAsMCwwLDM3LDNIMjdhMSwxLDAsMCwwLTEsLjg2bC0xLDcuMjlhMjIuMTEsMjIuMTEsMCwwLDAtNy40OCw0LjM1bC02Ljg3LTIuNzZhMSwxLDAsMCwwLTEuMjMuNDNsLTUsOC42NmExLDEsMCwwLDAsLjI0LDEuMjlsNS44MSw0LjU2YTIxLjQzLDIxLjQzLDAsMCwwLDAsOC42NEw0LjYzLDQwLjg4YTEsMSwwLDAsMC0uMjQsMS4yOWw1LDguNjZhMSwxLDAsMCwwLDEuMjMuNDNsNi44Ny0yLjc2QTIyLjExLDIyLjExLDAsMCwwLDI1LDUyLjg1bDEsNy4yOUExLDEsMCwwLDAsMjcsNjFIMzdhMSwxLDAsMCwwLDEtLjg2bDEtNy4yOWEyMi4xMSwyMi4xMSwwLDAsMCw3LjQ4LTQuMzVsNi44NywyLjc2YTEsMSwwLDAsMCwxLjIzLS40M2w1LTguNjZhMSwxLDAsMCwwLS4yNC0xLjI5bC01LjgxLTQuNTZhMjEuNDMsMjEuNDMsMCwwLDAsMC04LjY0bDUuODEtNC41NkExLDEsMCwwLDAsNTkuNjEsMjEuODNabS03Ljc4LDQuNjZhMSwxLDAsMCwwLS4zNiwxLDE5LjM3LDE5LjM3LDAsMCwxLDAsOSwxLDEsMCwwLDAsLjM2LDFsNS42Miw0LjQxLTQuMTMsNy4xNi02LjY0LTIuNjdhMSwxLDAsMCwwLTEuMDYuMiwyMC4wNiwyMC4wNiwwLDAsMS03Ljc4LDQuNTIsMSwxLDAsMCwwLS43LjgxbC0xLDcuMDZIMjcuODdsLTEtNy4wNmExLDEsMCwwLDAtLjctLjgxLDIwLjA2LDIwLjA2LDAsMCwxLTcuNzgtNC41MiwxLDEsMCwwLDAtMS4wNi0uMmwtNi42NCwyLjY3TDYuNTUsNDEuOTJsNS42Mi00LjQxYTEsMSwwLDAsMCwuMzYtMSwxOS4zNywxOS4zNywwLDAsMSwwLTksMSwxLDAsMCwwLS4zNi0xTDYuNTUsMjIuMDhsNC4xMy03LjE2LDYuNjQsMi42N2ExLDEsMCwwLDAsMS4wNi0uMiwyMC4wNiwyMC4wNiwwLDAsMSw3Ljc4LTQuNTIsMSwxLDAsMCwwLC43LS44MWwxLTcuMDZoOC4yNmwxLDcuMDZhMSwxLDAsMCwwLC43LjgxLDIwLjA2LDIwLjA2LDAsMCwxLDcuNzgsNC41MiwxLDEsMCwwLDAsMS4wNi4ybDYuNjQtMi42Nyw0LjEzLDcuMTZaIj48L3BhdGg+PHBhdGggZD0iTTMyLDE3QTE1LDE1LDAsMSwwLDQ3LDMyLDE1LDE1LDAsMCwwLDMyLDE3Wm0wLDI4QTEzLDEzLDAsMSwxLDQ1LDMyLDEzLDEzLDAsMCwxLDMyLDQ1WiI+PC9wYXRoPjwvc3ZnPg==" />
</a>`;
}
return /*html*/`
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
@ -106,6 +113,8 @@ app.addComponent('navbar', (store) => { return new Reef("#navbar", {
${refreshItem}
${settingsItem}
<a class="navbar-item has-text-right" data-onclick="navbar.logout">
<span>log out ${data.user ? data.user.name : ''}</span>
<img alt="log out" width="32" height="32" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHg9IjBweCIgeT0iMHB4Ij48dGl0bGU+QXJ0Ym9hcmQgODQ8L3RpdGxlPjxnPjxwYXRoIGQ9Ik0yOCw4MkgzOFY3NEgyOGEyLDIsMCwwLDEtMi0yVjI4YTIsMiwwLDAsMSwyLTJIMzhWMThIMjhBMTAsMTAsMCwwLDAsMTgsMjhWNzJBMTAsMTAsMCwwLDAsMjgsODJaIj48L3BhdGg+PHBhdGggZD0iTTY2LDMyLjM0LDYwLjM0LDM4bDgsOEgzNHY4SDY4LjM0bC04LDhMNjYsNjcuNjYsODAuODMsNTIuODNhNCw0LDAsMCwwLDAtNS42NloiPjwvcGF0aD48L2c+PC9zdmc+" />

17
package-lock.json generated
View file

@ -11,6 +11,7 @@
"better-sqlite3": "^7.1.2",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"eta": "^1.12.1",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"node-fetch": "^2.6.1",
@ -579,6 +580,17 @@
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"node_modules/eta": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/eta/-/eta-1.12.1.tgz",
"integrity": "sha512-H8npoci2J/7XiPnVcCVulBSPsTNGvGaINyMjQDU8AFqp9LGsEYS88g2CiU+d01Sg44WtX7o4nb8wUJ9vnI+tiA==",
"engines": {
"node": ">=6.0.0"
},
"funding": {
"url": "https://github.com/eta-dev/eta?sponsor=1"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -2059,6 +2071,11 @@
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"eta": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/eta/-/eta-1.12.1.tgz",
"integrity": "sha512-H8npoci2J/7XiPnVcCVulBSPsTNGvGaINyMjQDU8AFqp9LGsEYS88g2CiU+d01Sg44WtX7o4nb8wUJ9vnI+tiA=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",

View file

@ -12,6 +12,7 @@
"better-sqlite3": "^7.1.2",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"eta": "^1.12.1",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"node-fetch": "^2.6.1",

View file

@ -52,11 +52,17 @@ module.exports = async (req, res, next) => {
// skip auth for pub resources
// handle login and register paths
if ( req.originalUrl == "/favicon.ico" ){
res.sendFile(path.resolve("./client/pub/icons/favicon.ico"));
return;
}
if ( req.originalUrl.startsWith("/pub/") ){
next();
return;
} if ( req.method == "GET" && req.originalUrl == "/login" ){
res.type("html").sendFile(path.resolve('./templates/login.html'));
console.log("login");
// res.type("html").sendFile(path.resolve('./templates/login.html'));
res.render("login", { registerEnabled: dao.getProperty("registerEnabled") });
return;
} else if ( req.method == "POST" && req.originalUrl == "/login" ){
let username = req.body.username;
@ -86,15 +92,32 @@ module.exports = async (req, res, next) => {
res.redirect("./");
return;
} else if ( req.method == "GET" && req.originalUrl == "/register" ){
res.type("html").sendFile(path.resolve('./templates/register.html'));
let registerEnabled = dao.getProperty("registerEnabled");
if ( registerEnabled != "y" ){
res.sendStatus(403);
return;
}
res.render("register", {});
return;
} else if ( req.method == "POST" && req.originalUrl == "/register" ){
let registerEnabled = dao.getProperty("registerEnabled");
if ( registerEnabled != "y" ){
res.sendStatus(403);
return;
}
// if it's the first user, make them an admin - otherwise don't.
let userCount = dao.getUserCount();
let admin = userCount == 0 ? 1 : 0;
let username = req.body.username;
let salt = tokenUtils.createSalt();
let key = await tokenUtils.deriveKey(salt, req.body.password);
let result = dao.createUser(username, key, salt);
let result = dao.createUser(username, admin, key, salt);
if ( result && result.changes == 1 ){
sendAuthCookie(res, {
@ -115,6 +138,10 @@ module.exports = async (req, res, next) => {
// if we made it this far, we're eady to check for the cookie
let s = req.cookies.s;
// TODO: should probably check if the user's access has been revoked,
// but we currently don't allow deleting users anyway. A key rotation would
// be the other solution, but that would log out all users and require new tokens
// to be created.
if ( s ){
try {
s = tokenUtils.decrypt(s);

View file

@ -6,7 +6,7 @@ const conf = require('./conf.js');
let db = null;
function listBoards(userId){
let boards = db.prepare("SELECT * FROM boards").all();
let boards = db.prepare("SELECT * FROM boards where userId = @userId").all({userId:userId});
// get title pins
for( let i = 0; i < boards.length; ++i ){
@ -21,6 +21,10 @@ function listBoards(userId){
return boards;
}
function countBoardsByUser(userId){
return db.prepare("SELECT count(id) as count FROM boards WHERE userId = @userId").get({userId: userId});
}
function getBoard(userId, boardId){
let board = db.prepare("SELECT * FROM boards WHERE userId = @userId and id = @boardId").get({userId:userId, boardId:boardId});
@ -58,6 +62,10 @@ function listPins(userId, boardId){
return db.prepare("SELECT * FROM pins WHERE userId = @userId and boardId = @boardId").all({userId:userId, boardId:boardId});
}
function countPinsByUser(userId){
return db.prepare("SELECT count(id) as count FROM pins WHERE userId = @userId").get({userId:userId});
}
function getPin(userId, pinId){
return db.prepare('SELECT * FROM pins WHERE userId = @userId and id = @pinId').get({userId: userId, pinId:pinId});
}
@ -130,7 +138,24 @@ function deletePin(userId, pinId){
function getProperty(key){
// this will throw if the property does not exist
return db.prepare("SELECT value FROM properties WHERE key = ?").get('cookieKey').value;
let result = db.prepare("SELECT value FROM properties WHERE key = ?").get(key);
if ( result ){
return result.value;
}
return null;
}
function setProperty(key, value){
let result = db.prepare("UPDATE properties SET value = @value WHERE key = @key").run({key: key, value: value});
return result.changes == 1;
}
function getUser(userId){
return db.prepare("SELECT id, username, admin, createDate FROM users where id = ?").get(userId);
}
function listUsers(){
return db.prepare("SELECT id, username, admin, createDate FROM users").all();
}
function getSaltForUser(username){
@ -138,11 +163,26 @@ function getSaltForUser(username){
}
function getUserByNameAndKey(username, key){
return db.prepare("SELECT * FROM users WHERE username = @username AND key = @key").get({username: username, key: key});
return db.prepare("SELECT id, username, admin, createDate FROM users WHERE username = @username AND key = @key").get({username: username, key: key});
}
function createUser(username, key, salt){
return db.prepare("INSERT INTO users (username, key, salt, createDate) VALUES (@username, @key, @salt, @createDate)").run({username: username, key: key, salt: salt, createDate: new Date().toISOString()});
function createUser(username, admin, key, salt){
return db.prepare("INSERT INTO users (username, admin, key, salt, createDate) VALUES (@username, @admin, @key, @salt, @createDate)").run({username: username, admin: admin, key: key, salt: salt, createDate: new Date().toISOString()});
}
function getUserCount(){
return db.prepare("SELECT count(id) as count FROM users").get().count;
}
function setUserAdmin(userId, admin){
let result = db.prepare("UPDATE users SET admin = @admin WHERE id = @userId").run({userId:userId, admin:admin});
return result.changes == 1;
}
function deleteUser(userId){
db.prepare("DELETE FROM pins WHERE userId = ?").run(userId);
db.prepare("DELETE FROM boards WHERE userId = ?").run(userId);
db.prepare("DELETE FROM users WHERE id = ?").run(userId);
}
async function init(path){
@ -310,6 +350,47 @@ async function init(path){
schemaVersion = 3;
}
if ( schemaVersion < 4 ){
console.log(" running migration v4");
if ( !isNewDb && !createdBackup ){
let backupPath = DB_PATH + ".backup-" + new Date().toISOString();
console.log(" backing up to: " + backupPath);
db.prepare(`
VACUUM INTO ?
`).run(backupPath);
createdBackup = true;
}
db.transaction( () => {
db.prepare('ALTER TABLE users ADD COLUMN admin').run();
db.prepare('ALTER TABLE users ADD COLUMN uuid').run(); // need a uuid column to track real uniqueness, because we didn't use AUTOINCREMENT.
db.prepare("UPDATE users SET admin = 1").run();
let users = db.prepare("SELECT id FROM users").all();
for ( let i = 0; i < users.length; ++i ){
let uuid = crypto.randomBytes(16).toString("hex"); // not a real uuid, but serves the same purpose
db.prepare("UPDATE users SET uuid = @uuid WHERE id = @id").run({id: users[i].id, uuid: uuid});
}
db.prepare(`
INSERT INTO properties (key,value) VALUES (@key, @value)
`).run({key: 'registerEnabled', value: "y"});
db.prepare(`
INSERT INTO migrations (id, createDate) VALUES ( @id, @createDate )
`).run({id:4, createDate: new Date().toISOString()});
schemaVersion = 4;
})();
}
console.log(`database ready - schema version v${schemaVersion}`);
console.log('');
}
@ -318,18 +399,26 @@ async function init(path){
module.exports = {
init: init,
listBoards: listBoards,
countBoardsByUser, countBoardsByUser,
getBoard: getBoard,
findBoardByUserAndName: findBoardByUserAndName,
createBoard: createBoard,
updateBoard: updateBoard,
deleteBoard: deleteBoard,
listPins: listPins,
countPinsByUser: countPinsByUser,
getPin: getPin,
createPin: createPin,
updatePin: updatePin,
deletePin: deletePin,
getProperty: getProperty,
setProperty: setProperty,
getUser: getUser,
listUsers: listUsers,
getSaltForUser: getSaltForUser,
getUserCount: getUserCount,
getUserByNameAndKey: getUserByNameAndKey,
createUser: createUser
createUser: createUser,
deleteUser: deleteUser,
setUserAdmin: setUserAdmin
};

View file

@ -7,6 +7,8 @@ const tokenUtil = require('./token-utils.js');
const dao = require("./dao.js");
const conf = require("./conf.js");
const imageUtils = require('./image-utils.js');
var eta = require("eta");
const tokenUtils = require('./token-utils.js');
module.exports = async () => {
@ -77,6 +79,9 @@ module.exports = async () => {
// express config
const app = express();
app.engine("eta", eta.renderFile);
app.set("view engine", "eta");
app.set("views", "./templates")
const expressWs = require('express-ws')(app);
app.use(bodyParser.raw({type: 'image/jpeg', limit: '25mb'})); // accept image/jpeg files only
@ -131,7 +136,12 @@ module.exports = async () => {
const SERVER_ERROR = {status: "error", error: "server error"};
app.get("/api/whoami", (req, res) => {
res.send({name: req.user.name, version: VERSION, id: req.user.id});
let user = dao.getUser(req.user.id);
if ( !user ){
res.sendStatus(403);
return;
}
res.send({name: user.username, id: user.id, admin: user.admin, version: VERSION});
});
// list boards
@ -333,6 +343,9 @@ module.exports = async () => {
app.post("/up", async (req, res) => {
try {
require("fs").writeFileSync("up.jpg", req.body);
// try to parse the image first... if this blows up we'll stop early
let image = await imageUtils.processImage(req.body);
@ -358,6 +371,117 @@ module.exports = async () => {
}
});
app.get("/api/apikey", (req,res) => {
let s = req.cookies['s'];
console.log("s=" + s);
res.send({apiKey: s});
});
app.get("/settings", (req, res) => {
let user = dao.getUser(req.user.id);
if ( user.admin != "y" ){
res.sendStatus(403);
return;
}
let registerEnabled = dao.getProperty("registerEnabled");
let users = dao.listUsers();
for ( let i = 0; i < users.length; ++i ){
users[i].boardCount = dao.countBoardsByUser(users[i].id).count;
users[i].pinCount = dao.countPinsByUser(users[i].id).count;
}
res.render("settings", {
registerEnabled: registerEnabled,
users: users,
userId: req.user.id
});
});
app.post("/settings", async (req, res) => {
let user = dao.getUser(req.user.id);
if ( user.admin != "y" ){
res.sendStatus(403);
return;
}
if ( req.body.action == "updateUsers" ){
let users = dao.listUsers();
for ( let i = 0; i < users.length; ++i ){
if ( users[i].id != req.user.id ){ // can't update yourself
let adminSetting = req.body['admin-' + users[i].id];
if ( adminSetting != users[i].admin ){
dao.setUserAdmin(users[i].id, adminSetting);
}
}
}
res.redirect("./settings#users-updated");
return;
} else if ( req.body.action == "updateSettings" ){
let registerEnabled = 'y';
if ( req.body.registerEnabled == "n" ){
registerEnabled = 'n';
}
dao.setProperty('registerEnabled', registerEnabled);
res.redirect("./settings#settings-updated");
return;
} else if ( req.body.action == "createUser" ){
let username = req.body.username;
let password = req.body.password;
let repeatPassword = req.body.repeatPassword;
console.log(`username: ${username} password: ${password} rp: ${repeatPassword}`);
if ( password != repeatPassword ){
res.redirect("./settings#password-match")
return;
}
let salt = tokenUtils.createSalt();
let key = await tokenUtils.deriveKey(salt, password);
try{
dao.createUser(username, 'n', key, salt);
} catch (err){
console.log("error creating user " + username, err);
res.redirect("./settings#create-user-error");
return;
}
res.redirect("./settings#created-user");
return;
} else if ( req.body.action == "deleteUser" ){
let uid = req.body.uid;
try {
dao.deleteUser(uid);
require("fs").rmdirSync(conf.getImagePath() + "/" + uid , { recursive: true });
} catch (err){
console.log("error deleting user " + uid, err);
res.redirect("./settings#delete-user-error");
return;
}
res.redirect("./settings#deleted-user");
return;
}
res.redirect("./settings");
});
// start listening

View file

@ -41,12 +41,14 @@
<input class="input" name="password" id="password" type="password">
</div>
</div>
</section>
<footer class="modal-card-foot">
<button id="submitButton" class="button is-success" type="submit">login</button>
<% if ( it.registerEnabled == "y" ){ %>
<a class="button" href="register">create account</a>
<% } %>
<span id="nope"></span>
</footer>
</form>

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<title>tinypin</title>

267
templates/settings.eta Normal file
View file

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html>
<head>
<title>tinypin > settings</title>
<meta charset="utf-8">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="apple-touch-icon" sizes="180x180" href="pub/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="pub/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="pub/icons/favicon-16x16.png">
<link rel="manifest" href="pub/icons/site.webmanifest">
<link rel="mask-icon" href="pub/icons/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="pub/icons/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="pub/icons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="pub/bulma-custom.css" />
</head>
<body>
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="./">
<img alt="boards" style="width:24px; height: 24px;" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2OCA0OCIgeD0iMHB4IiB5PSIwcHgiPjxwYXRoIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTcyLDgwNiBMMTA3LDgwNiBDMTA4LjEwNDU2OSw4MDYgMTA5LDgwNi44OTU0MzEgMTA5LDgwOCBMMTA5LDgzMiBDMTA5LDgzMy4xMDQ1NjkgMTA4LjEwNDU2OSw4MzQgMTA3LDgzNCBMNzIsODM0IEM3MC44OTU0MzA1LDgzNCA3MCw4MzMuMTA0NTY5IDcwLDgzMiBMNzAsODA4IEM3MCw4MDYuODk1NDMxIDcwLjg5NTQzMDUsODA2IDcyLDgwNiBaIE0xMTIsODIyIEwxMTIsODIyIEwxMTIsODA4IEMxMTIsODA1LjIzODU3NiAxMDkuNzYxNDI0LDgwMyAxMDcsODAzIEw5Niw4MDMgTDk2LDc4OCBDOTYsNzg2Ljg5NTQzMSA5Ni44OTU0MzA1LDc4NiA5OCw3ODYgTDEyMiw3ODYgQzEyMy4xMDQ1NjksNzg2IDEyNCw3ODYuODk1NDMxIDEyNCw3ODggTDEyNCw4MjAgQzEyNCw4MjEuMTA0NTY5IDEyMy4xMDQ1NjksODIyIDEyMiw4MjIgTDExMiw4MjIgWiBNODQsODAzIEw3Miw4MDMgQzY5LjIzODU3NjMsODAzIDY3LDgwNS4yMzg1NzYgNjcsODA4IEw2Nyw4MTcgTDU4LDgxNyBDNTYuODk1NDMwNSw4MTcgNTYsODE2LjEwNDU2OSA1Niw4MTUgTDU2LDc5MSBDNTYsNzg5Ljg5NTQzMSA1Ni44OTU0MzA1LDc4OSA1OCw3ODkgTDgyLDc4OSBDODMuMTA0NTY5NSw3ODkgODQsNzg5Ljg5NTQzMSA4NCw3OTEgTDg0LDgwMyBaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNTYgLTc4NikiPjwvcGF0aD48L3N2Zz4=" />
</a>
<span class="navbar-item">
<span>Settings</span>
</span>
<a role="button" class="navbar-burger is-active" aria-label="menu" aria-expanded="false" href="./">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
</nav>
<section class="section" id="settings-updated" style="display: none;">
<div class="notification is-success">
Settings updated successfully.
</div>
</section>
<section class="section" id="users-updated" style="display: none;">
<div class="notification is-success">
Users updated successfully.
</div>
</section>
<section class="section" id="create-user-error" style="display: none;">
<div class="notification is-danger">
Error creating user.
</div>
</section>
<section class="section" id="created-user" style="display: none;">
<div class="notification is-success">
Created user.
</div>
</section>
<section class="section">
<div class="box">
<h1 style="border-bottom: 1px solid #eee;"><strong>Settings</strong></h1>
<br />
<form method="POST" action="./settings">
<input type="hidden" name="action" value="updatePreferences">
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label has-text-weight-normal">Registration: </label>
</div>
<div class="field-body">
<div class="control">
<div class="select">
<select name="registerEnabled">
<option value="y" <% if ( it.registerEnabled == "y" ){ %> selected <% } %> >Enabled</option>
<option value="n" <% if ( it.registerEnabled != "y" ) { %> selected <% }%> >Disabled</option>
</select>
</div>
</div>
</div>
</div>
<div class="has-text-right">
<button class="button is-success">Save</button>
</div>
</form>
</div>
</section>
<div class="section">
<div class="box">
<h1 style="border-bottom: 1px solid #eee;"><strong>Users</strong></h1>
<br />
<form method="POST" action="./settings">
<input type="hidden" name="action" value="updateUsers">
<table class="table" style="width: 100%;">
<thead>
<tr>
<th>id</th>
<th>username</th>
<th>admin</th>
<th>created</th>
<th>boards</th>
<th>pins</th>
<th>&nbsp;</th>
</tr>
<thead>
<tbody>
<% for ( let i = 0; i < it.users.length; ++i ){ let user = it.users[i]; %>
<tr>
<td><%= user.id %></td>
<td><%= user.username %></td>
<td>
<% if (it.userId != user.id) { %>
<div class="select is-small">
<select name="admin-<%= user.id %>">
<option value="y" <% if (user.admin == "y" ){ %> selected <% } %> >y</option>
<option value="n" <% if (user.admin != "y" ){ %> selected <% } %> >n</option>
</select>
</div>
<% } else { %>
<%= user.admin %>
<% } %>
</td>
<td><%= user.createDate %></td>
<td><%= user.boardCount %></td>
<td><%= user.pinCount %></td>
<td>
<% if (it.userId != user.id) { %>
<a class="button is-small is-danger is-light" onclick="deleteUser(<%= user.id %>)">Delete User</a>
<% } %>
</td>
</tr>
<% } %>
</tbody>
</table>
<div class="level">
<div class="level-left">
<a class="button" onclick="createUser()">Create User</a>
</div>
<div class="level-right">
<button class="button is-success">Save</button>
</div>
</div>
</form>
</div>
</div>
<div class="modal" id="create-user-modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">tinypin &raquo; create account</p>
</header>
<form method="post" action="./settings" onsubmit="return submitCreateUserForm()">
<input type="hidden" name="action" value="createUser" />
<section class="modal-card-body">
<div class="field">
<label class="label" for="username">username</label>
<div class="control">
<input class="input" name="username" id="username" type="text">
</div>
</div>
<div class="field">
<label class="label" for="password">password</label>
<div class="control">
<input class="input" name="password" id="password" type="password">
</div>
</div>
<div class="field">
<label class="label" for="repeatPassword">repeat password</label>
<div class="control">
<input class="input" name="repeatPassword" id="repeatPassword" type="password">
</div>
</div>
</section>
<footer class="modal-card-foot">
<button id="submitButton" class="button is-success" type="submit">create account</button>
<a id="cancelButton" class="button" onclick="createUserCancel()">Cancel</a>
<span id="createUserError"></span>
</footer>
</form>
</div>
</div>
<form id="deleteUserForm" action="./settings" method="POST">
<input type="hidden" name="action" value="deleteUser" />
<input type="hidden" id="deleteUserUid" name="uid" value="" />
</form>
<script>
if ( window.location.hash == "#settings-updated" ){
document.getElementById("settings-updated").style.display = "block";
} else if ( window.location.hash =="#users-updated" ){
document.getElementById("users-updated").style.display = "block";
} else if (window.location.hash == "#created-user" ){
document.getElementById("created-user").style.display = "block";
} else if ( window.location.hash == "#create-user-error" ){
document.getElementById("create-user-error").style.display = "block";
}
function createUser(){
document.getElementById("create-user-modal").classList.add("is-active");
}
function createUserCancel(){
document.getElementById("create-user-modal").classList.remove("is-active");
document.getElementById("username").value = "";
document.getElementById("password").value = "";
document.getElementById("repeatPassword").value = "";
}
function submitCreateUserForm(){
let createUserError = document.getElementById("createUserError");
let username = document.getElementById("username").value;
let password = document.getElementById("password").value;
let repeatPassword = document.getElementById("repeatPassword").value;
if ( username.trim().length < 1 ){
createUserError.innerText = "username is required";
return false;
} else if ( password.trim().length < 1 ){
createUserError.innerText = "password is required";
return false;
} else if ( password != repeatPassword ){
createUserError.innerText = "passwords don't match";
return false;
}
createUserError.innerText = "";
return true;
}
function deleteUser(uid){
if ( window.confirm("Are you sure you want to delete this user? All associated data will be deleted.") ){
document.getElementById("deleteUserUid").value = uid;
document.getElementById("deleteUserForm").submit();
}
}
</script>
</body>
</html>

48
utils/change-password.js Normal file
View file

@ -0,0 +1,48 @@
const crypto = require('crypto');
const readline = require("readline");
const writable = require('stream').Writable;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true
});
rl.stdoutMuted = true;
rl.query = "password: ";
rl._writeToOutput = function _writeToOutput(stringToWrite) {
if ( stringToWrite == rl.query ){
rl.output.write(stringToWrite);
}
// if (rl.stdoutMuted)
// // rl.output.write("");
// // rl.output.write("\x1B[2K\x1B[200D"+rl.query+"["+((rl.line.length%2==1)?"=-":"-=")+"]");
// else
// rl.output.write(stringToWrite);
};
async function deriveKey(salt, pw){
return new Promise( (resolve, reject) => {
crypto.scrypt(pw, salt, 64, (err, key) => {
resolve(key.toString('hex'));
});
});
}
function createSalt(){
return crypto.randomBytes(16).toString('hex');
}
// let username = req.body.username;
// let salt = createSalt();
// let key = await deriveKey(salt, req.body.password);
rl.question(rl.query, async (password) => {
rl.close();
console.log(password);
let salt = createSalt();
let key = await deriveKey(salt, password);
console.log("salt: " + salt);
console.log("key: " + key);
});

16
utils/image-test.js Normal file
View file

@ -0,0 +1,16 @@
const sharp = require("sharp");
async function run(){
let original = await sharp("/Users/slynn1324/Desktop/IMG_5166.HEIC");
console.log(await original.metadata());
let jpg = await original.toFormat("jpg").toBuffer();
jpg = await sharp(jpg);
console.log(await jpg.metadata() );
}
run();

3
utils/rotate-key.js Normal file
View file

@ -0,0 +1,3 @@
const crypto = require('crypto');
console.log("cookieKey: " + crypto.randomBytes(32).toString('hex'));