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

@ -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