James Ketrenos 652a76bdd9 Fix use of user before initialized
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
2023-01-12 15:55:05 -08:00

390 lines
11 KiB
JavaScript
Executable File

"use strict";
const express = require("express"),
config = require("config"),
LdapAuth = require("ldapauth-fork"),
{ sendVerifyMail, sendPasswordChangedMail } = require("../lib/mail"),
crypto = require("crypto");
const router = express.Router();
let userDB;
let ldap;
if (config.has("ldap.url")) {
ldap = new LdapAuth(Object.assign({}, config.get("ldap")));
} else {
ldap = null;
}
require("../db/users").then(function(db) {
userDB = db;
});
router.get("/", function(req, res/*, next*/) {
console.log("/users/");
return getSessionUser(req).then(function(user) {
return res.status(200).send(user);
}).catch(function(error) {
console.log("User not logged in: " + error);
return res.status(200).send({});
});
});
function ldapPromise(username, password) {
if (!ldap) {
return Promise.reject("LDAP not being used");
}
return new Promise(function(resolve, reject) {
ldap.authenticate(username.replace(/@.*$/, ""), password, function(error, user) {
if (error) {
return reject(error);
}
return resolve(user);
});
});
}
const ldapJS = require("ldapjs"),
ldapConfig = config.get("ldap");
const ldapSetPassword = function(username, password) {
const client = ldapJS.createClient({
url: ldapConfig.url
});
return new Promise(function(resolve, reject) {
client.bind(ldapConfig.bindDn, ldapConfig.bindCredentials, function(err) {
if (err) {
return reject("Error binding to LDAP: " + err);
}
var change = new ldapJS.Change({
operation: "replace",
modification: {
userPassword : password,
}
});
client.modify("uid=" + username + ",ou=people," + ldapConfig.searchBase, change, function(err) {
if (err) {
return reject("Error changing password: " + err);
}
return resolve();
});
});
}).catch(function(error) {
console.error(error);
}).then(function() {
client.unbind(function(err) {
if (err) {
console.error("Error unbinding: " + err);
}
});
});
};
router.put("/password", function(req, res) {
console.log("/users/password");
const changes = {
currentPassword: req.query.c || req.body.c,
newPassword: req.query.n || req.body.n
};
if (!changes.currentPassword || !changes.newPassword) {
return res.status(400).send("Missing current password and/or new password.");
}
if (changes.currentPassword == changes.newPassword) {
return res.status(400).send("Attempt to set new password to current password.");
}
return getSessionUser(req).then(function(user) {
if (req.session.userId == "LDAP") {
return ldapPromise(user.username, changes.currentPassword).then(function() {
return user;
}).catch(function() {
return null;
});
}
/* Not an LDAP user, so query in the DB */
return userDB.sequelize.query("SELECT id FROM users " +
"WHERE uid=:username AND password=:password", {
replacements: {
username: user.username,
password: crypto.createHash('sha256').update(changes.currentPassword).digest('base64')
},
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(users) {
if (users.length != 1) {
return null;
}
return user;
});
}).then(function(user) {
if (!user) {
console.log("Invalid password");
/* Invalid password */
res.status(401).send("Invalid password");
return null;
}
let updatePromise;
if (req.session.userId == "LDAP") {
updatePromise = ldapSetPassword(user.username, changes.newPassword);
} else {
updatePromise = userDB.sequelize.query("UPDATE users SET password=:password WHERE uid=:username", {
replacements: {
username: user.username,
password: crypto.createHash('sha256').update(changes.newPassword).digest('base64')
}
});
}
updatePromise.then(function() {
console.log("Password changed for user " + user.username + " to '" + changes.newPassword + "'.");
res.status(200).send(user);
user.id = req.session.userId;
return sendPasswordChangedMail(userDB, req, user);
});
});
});
router.post("/create", async (req, res) => {
console.log("/users/create");
const user = {
uid: req.query.m || req.body.m,
displayName: req.query.n || req.body.n || "",
password: req.query.p || req.body.p || "",
mail: req.query.m || req.body.m,
notes: req.query.w || req.body.w || ""
};
if (!user.uid || !user.password || !user.displayName || !user.notes) {
return res.status(400).send("Missing email address, password, name, and/or who you know.");
}
user.password = crypto.createHash('sha256').update(user.password).digest('base64');
return userDB.sequelize.query("SELECT * FROM users WHERE uid=:uid", {
replacements: user,
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(results) {
if (results.length != 0) {
return res.status(400).send("Email address already used.");
}
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!re.exec(user.mail)) {
console.log("Invalid email address: " + user.mail);
throw "Invalid email address.";
}
}).then(async () => {
const [ results, metadata ] = await userDB.sequelize.query(
"INSERT INTO users " +
"(uid,displayName,password,mail,memberSince,authenticated,notes) " +
"VALUES(:uid,:displayName,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", {
replacements: user
});
req.session.userId = metadata.lastID;
const tmp = await getSessionUser(req);
res.status(200).send(tmp);
tmp.id = req.session.userId;
return sendVerifyMail(userDB, req, tmp);
}).catch(function(error) {
console.log("Error creating account: ", error);
return res.status(401).send(error);
});
});
const getSessionUser = function(req) {
return Promise.resolve().then(function() {
if (!req.session || !req.session.userId) {
throw "Unauthorized. You must be logged in.";
}
if (req.session.userId == "LDAP") {
if (req.session.ldapUser) {
return req.session.ldapUser;
}
req.session.userId = null;
req.session.ldapUser = null;
throw "Invalid LDAP session";
}
let query = "SELECT " +
"uid AS username,displayName,mailVerified,authenticated,memberSince AS name,mail " +
"FROM users WHERE id=:id";
return userDB.sequelize.query(query, {
replacements: {
id: req.session.userId
},
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(results) {
if (results.length != 1) {
throw "Invalid account.";
}
let user = results[0];
if (!user.mailVerified) {
user.restriction = user.restriction || "Email address not verified.";
return user;
}
if (!user.authenticated) {
user.restriction = user.restriction || "Accout not authorized.";
return user;
}
return user;
});
}).then(function(user) {
req.user = user;
/* If the user already has a restriction, or there are no album user restrictions,
* return the user to the next promise */
if (user.restriction || !config.has("restrictions")) {
return user;
}
let allowed = config.get("restrictions");
if (!Array.isArray(allowed)) {
allowed = [ allowed ];
}
for (let i = 0; i < allowed.length; i++) {
if (allowed[i] == user.username) {
return user;
}
}
console.log("Unauthorized (logged in) access by user: " + user.username);
user.restriction = "Unauthorized access attempt to restricted album.";
return user;
}).then(function(user) {
/* If there are maintainers on this album, check if this user is a maintainer */
if (config.has("maintainers")) {
let maintainers = config.get("maintainers");
if (maintainers.indexOf(user.username) != -1) {
user.maintainer = true;
if (user.restriction) {
console.warn("User " + user.username + " is a maintainer AND has a restriction which will be ignored: " + user.restriction);
delete user.restriction;
}
}
}
return user;
}).then(function(user) {
/* Strip out any fields that shouldn't be there. The allowed fields are: */
let allowed = [
"maintainer", "username", "displayName", "mailVerified", "authenticated", "name", "mail", "restriction"
];
for (let field in user) {
if (allowed.indexOf(field) == -1) {
delete user[field];
}
}
return user;
});
}
router.post("/login", function(req, res) {
console.log("/users/login");
let username = req.query.u || req.body.u || "",
password = req.query.p || req.body.p || "";
console.log("Login attempt");
if (!username || !password) {
return res.status(400).send("Missing username and/or password");
}
/* We use LDAP as the primary authenticator; if the user is not
* found there, we look the user up in the site-specific user database */
return ldapPromise(username, password).then(function(ldap) {
let user = {};
user.id = "LDAP";
user.displayName = ldap.displayName;
user.username = ldap.uid;
user.mail = ldap.mail;
user.authenticated = 1;
user.mailVerified = 1;
req.session.userId = "LDAP";
req.session.ldapUser = user;
return user;
}).catch(function(error) {
console.warn(error);
console.log("User not found in LDAP. Looking up in DB.");
let query = "SELECT " +
"id,mailVerified,authenticated,uid AS username,displayName AS name,mail " +
"FROM users WHERE uid=:username AND password=:password";
return userDB.sequelize.query(query, {
replacements: {
username: username,
password: crypto.createHash('sha256').update(password).digest('base64')
},
type: userDB.Sequelize.QueryTypes.SELECT
}).then(function(users) {
if (users.length != 1) {
return null;
}
let user = users[0];
req.session.userId = user.id;
return user;
});
}).then(function(user) {
if (!user) {
console.log(username + " not found (or invalid password.)");
req.session.userId = null;
return res.status(401).send("Invalid login credentials");
}
let message = "Logged in as " + user.username + " (" + user.id + ")";
if (!user.mailVerified) {
console.log(message + ", who is not verified email.");
} else if (!user.authenticated) {
console.log(message + ", who is not authenticated.");
} else {
console.log(message);
}
return getSessionUser(req).then(function(user) {
return res.status(200).send(user);
});
}).catch(function(error) {
console.log(error);
return res.status(403).send(error);
});
});
router.get("/logout", function(req, res) {
console.log("/users/logout");
if (req.session && req.session.userId) {
if (req.session.userId == "LDAP") {
req.session.ldapUser = null;
}
req.session.userId = null;
}
res.status(200).send({});
});
module.exports = {
router,
getSessionUser
};