2018-10-04 17:19:04 -07:00

297 lines
8.3 KiB
JavaScript
Executable File

"use strict";
const express = require("express"),
config = require("config"),
LdapAuth = require("ldapauth-fork"),
crypto = require("crypto"),
createTransport = require("nodemailer").createTransport,
hb = require("handlebars");
const router = express.Router();
let userDB;
let transporter = createTransport({
host: config.get("smtp.host"),
pool: true,
port: config.has("smtp.port") ? config.get("smtp.port") : 25
});
let ldap;
if (config.has("ldap.url")) {
ldap = new LdapAuth(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) {
req.user = user;
return res.status(200).send(req.user);
});
});
const templates = {
"html": [
"<p>Dear {{username}},</p>",
"",
"<p>Welcome to <b>ketrenos.com</b>. Before you can access the system, you must authenticate",
"your email address ({{mail}}).</p>",
"",
"<p>To do so, simply access this link, which contains your authentication ",
"token: <a href=\"{{url}}{{secret}}\">{{secret}}</a></p>",
"",
"<p>Sincerely,</p>",
"<p>James</p>"
].join("\n"),
"text": [
"Dear {{username}},",
"",
"Welcome to ketrenos.com. Before you can access the system, you must authenticate",
"your email address ({{mail}}).",
"",
"To do so, simply access this link, which contains your authentication ",
"token: {{url}}{{secret}}",
"",
"Sincerely,",
"James"
].join("\n")
};
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);
});
});
}
router.post("/create", function(req, res) {
console.log("/users/create");
let who = req.query.w || req.body.w || "",
password = req.query.p || req.body.p || "",
name = req.query.n || req.body.n || "",
mail = req.query.m || req.body.m;
if (!who || !password || !mail || !name) {
return res.status(400).send("Missing who you know, name, password, and/or email");
}
let query = "SELECT * FROM users WHERE uid=:username";
return userDB.sequelize.query(query, {
replacements: {
username: mail
},
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(mail)) {
console.log("Invalid email address: " + mail);
return res.status(400).send("Invalid email address.");
}
}).then(function() {
return new Promise(function(resolve, reject) {
crypto.randomBytes(16, function(error, buffer) {
if (error) {
return reject(error);
}
return resolve(buffer.toString('hex'));
});
});
}).then(function(secret) {
return userDB.sequelize.query("INSERT INTO users " +
"(uid,displayName,password,mail,memberSince,authenticated,notes) " +
"VALUES(:username,:name,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", {
replacements: {
username: mail,
name: name,
password: crypto.createHash('sha256').update(password).digest('base64'),
mail: mail,
notes: who
}
}).spread(function(results, metadata) {
req.session.userId = metadata.lastID;
return userDB.sequelize.query("INSERT INTO authentications " +
"(userId,issued,key,type) VALUES " +
"(:userId,CURRENT_TIMESTAMP,:key,'account-setup')", {
replacements: {
key: secret,
userId: req.session.userId
}
}).catch(function(error) {
console.log(error);
throw error;
});
}).then(function() {
let data = {
username: name,
mail: mail,
secret: secret,
url: req.protocol + "://" + req.hostname + req.app.get("basePath")
}, envelope = {
to: mail,
from: config.get("smtp.sender"),
subject: "Request to ketrenos.com create account for '" + name + "'",
cc: "",
bcc: config.get("admin.mail"),
text: hb.compile(templates.text)(data),
html: hb.compile(templates.html)(data)
};
return new Promise(function (resolve, reject) {
let attempts = 10;
function send(envelope) {
/* Rate limit to ten per second */
transporter.sendMail(envelope, function (error, info) {
if (!error) {
console.log('Message sent: ' + info.response);
return resolve();
}
if (attempts == 0) {
console.log("Error sending email: ", error)
return reject(error);
}
attempts--;
console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error);
setTimeout(send.bind(undefined, envelope), 100);
});
}
send(envelope);
});
}).then(function() {
return getSessionUser(req).then(function(user) {
return res.status(200).send(user);
});
});
});
});
const getSessionUser = function(req) {
if (!req.session.userId) {
return Promise.resolve({});
}
if (req.session.userId == "LDAP") {
return Promise.resolve(req.session.ldapUser);
}
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) {
return {};
}
return results[0];
});
}
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(user) {
user.name = user.displayName;
user.username = user.uid;
user.id = "LDAP";
user.mail = user.mail;
user.authenticated = 1;
user.mailVerified = 1;
req.session.userId = "LDAP";
req.session.ldapUser = user;
return user;
}).catch(function() {
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.name + " (" + 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);
}
delete user.id;
return res.status(200).send(user);
});
});
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;