"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 };