From 814bac8b787564a35bf5a9f47966fb4c602d67f6 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Wed, 17 Oct 2018 22:53:39 -0700 Subject: [PATCH] Applied DRY to authentication mail. Added user.restriction to indicate if an account username/password is correct, but the account is under restriction for various reasons. Signed-off-by: James Ketrenos --- frontend/src/ketr-photos/ketr-photos.html | 16 +- server/app.js | 5 +- server/lib/mail.js | 116 +++++++++++++++ server/routes/users.js | 174 ++++++---------------- util/resend.js | 133 +++-------------- 5 files changed, 192 insertions(+), 252 deletions(-) create mode 100644 server/lib/mail.js diff --git a/frontend/src/ketr-photos/ketr-photos.html b/frontend/src/ketr-photos/ketr-photos.html index c082222..e961242 100755 --- a/frontend/src/ketr-photos/ketr-photos.html +++ b/frontend/src/ketr-photos/ketr-photos.html @@ -286,6 +286,7 @@ } #loginStatus .status { + white-space: pre-line; margin-top: 0.5em; } @@ -1786,7 +1787,9 @@ return; } - if (user.authenticated && user.mailVerified) { + this.username = user.username; + + if (!user.restriction) { this.loginStatus = null; this.mode = "memories"; this.setActions(); @@ -1796,14 +1799,15 @@ this.actions = []; this.mode = "login"; + this.loginStatusTitle = user.restriction; if (!user.mailVerified) { - this.loginStatusTitle = "Account not verified"; - this.loginStatus = "An email has been sent to " + user.mail + ". " + + this.loginStatus = "An email has been sent to " + user.mail + ".\n\n" + "Click the link in that email to verify your email address."; } else if (!user.authenticated) { - this.loginStatusTitle = "Account not authorized"; - this.loginStatus = "Your email address has been verified. Next, James needs to authorize your account. " + - "He has received an email and will process the request as quickly as he can."; + this.loginStatus = + "The site admin needs to authorize your account before you can access the system.\n" + + "\n" + + "They have received an email and will process the request as quickly as possible."; } }, diff --git a/server/app.js b/server/app.js index 6ed8ed9..26d89cb 100755 --- a/server/app.js +++ b/server/app.js @@ -247,10 +247,13 @@ app.use(function(err, req, res, next) { /* Check authentication */ app.use(basePath, function(req, res, next) { return users.getSessionUser(req).then(function(user) { + if (user.restriction) { + return res.status(401).send(user.restriction); + } req.user = user; return next(); }).catch(function(error) { - return res.status(401).send(error); + return res.status(403).send(error); }); }); diff --git a/server/lib/mail.js b/server/lib/mail.js new file mode 100644 index 0000000..f727918 --- /dev/null +++ b/server/lib/mail.js @@ -0,0 +1,116 @@ +"use strict"; + +const config = require("config"), + crypto = require("crypto"), + hb = require("handlebars"); + +const templates = { + "html": [ + "

Hello {{username}},

", + "", + "

Welcome to ketrenos.com. You are almost done creating your account. ", + "Before you can access the system, you must verify your email address.

", + "", + "

To do so, simply access this link:

", + "

VERIFY {{mail}} ADDRESS

", + "", + "

Sincerely,

", + "

James

" + ].join("\n"), + "text": [ + "Hello {{username}},", + "", + "Welcome to ketrenos.com. You are almost done creating your account. ", + "Before you can access the system, you must verify your email address.", + "", + "To do so, simply access this link:", + "", + "{{url}}{{secret}}", + "", + "Sincerely,", + "James" + ].join("\n") +}; + +const sendVerifyMail = function(userDB, req, user) { + return userDB.sequelize.query("DELETE FROM authentications WHERE userId=:id AND type='account-setup'", { + replacements: { + id: user.id + } + }).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 authentications " + + "(userId,issued,key,type) " + + "VALUES (:userId,CURRENT_TIMESTAMP,:key,'account-setup')", { + replacements: { + key: secret, + userId: user.id + } + }).then(function() { + return secret; + }).catch(function(error) { + console.log(error); + throw error; + }); + }).then(function(secret) { + const transporter = req.app.get("transporter"); + if (!transporter) { + console.log("Not sending VERIFY email; SMTP not configured."); + return; + } + + let data = { + username: user.displayName, + mail: user.mail, + secret: secret, + url: req.protocol + "://" + req.hostname + req.app.get("basePath") + }, envelope = { + to: data.mail, + from: config.get("smtp.sender"), + subject: "Request to ketrenos.com create account for '" + data.username + "'", + 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); + }); + }).catch(function(error) { + console.log("Error creating account: ", error); + }); +}; + +module.exports = { + sendVerifyMail +} diff --git a/server/routes/users.js b/server/routes/users.js index 402c540..fd5dde9 100755 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -3,8 +3,8 @@ const express = require("express"), config = require("config"), LdapAuth = require("ldapauth-fork"), - crypto = require("crypto"), - hb = require("handlebars"); + { sendVerifyMail } = require("../lib/mail"), + crypto = require("crypto"); const router = express.Router(); @@ -31,34 +31,6 @@ router.get("/", function(req, res/*, next*/) { }); }); -const templates = { - "html": [ - "

Hello {{username}},

", - "", - "

Welcome to ketrenos.com. You are almost done creating your account. ", - "Before you can access the system, you must verify your email address.

", - "", - "

To do so, simply access this link:

", - "

VERIFY {{mail}} ADDRESS

", - "", - "

Sincerely,

", - "

James

" - ].join("\n"), - "text": [ - "Hello {{username}},", - "", - "Welcome to ketrenos.com. You are almost done creating your account. ", - "Before you can access the system, you must verify your email address.", - "", - "To do so, simply access this link:", - "", - "{{url}}{{secret}}", - "", - "Sincerely,", - "James" - ].join("\n") -}; - function ldapPromise(username, password) { if (!ldap) { return Promise.reject("LDAP not being used"); @@ -76,20 +48,22 @@ function ldapPromise(username, password) { 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; + 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 (!who || !password || !mail || !name) { - return res.status(400).send("Missing who you know, name, password, and/or email"); + if (!user.uid || !user.password || !user.displayName || !user.notes) { + return res.status(400).send("Missing email address, password, name, and/or who you know."); } - let query = "SELECT * FROM users WHERE uid=:username"; - return userDB.sequelize.query(query, { - replacements: { - username: mail - }, + 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) { @@ -98,97 +72,25 @@ router.post("/create", function(req, res) { } 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); + if (!re.exec(user.mail)) { + console.log("Invalid email address: " + user.mail); throw "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 - } + "(uid,displayName,password,mail,memberSince,authenticated,notes) " + + "VALUES(:uid,:displayName,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", { + replacements: user }).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() { - const transporter = req.app.get("transporter"); - if (!transporter) { - console.log("Not sending VERIFY email; SMTP not configured."); - return; - } - - 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); - }).catch(function(error) { - console.log("Error creating account: ", error); - return res.status(401).send(error); + res.status(200).send(user); + return sendVerifyMail(userDB, req, user); }); + }).catch(function(error) { + console.log("Error creating account: ", error); + return res.status(401).send(error); }); }); }); @@ -224,18 +126,23 @@ const getSessionUser = function(req) { let user = results[0]; req.user = user; - if (!user.authenticated) { - throw "Accout not authenticated."; - } if (!user.mailVerified) { - throw "Account mail not verified."; + 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) { - if (!config.has("restrictions")) { + /* 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; } @@ -249,12 +156,19 @@ const getSessionUser = function(req) { } } console.log("Unauthorized (logged in) access by user: " + req.user.username); - throw "Unauthorized access attempt to restricted album."; + 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; + } } } @@ -262,7 +176,7 @@ const getSessionUser = function(req) { }).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" + "maintainer", "username", "displayName", "mailVerified", "authenticated", "name", "mail", "restriction" ]; for (let field in user) { if (allowed.indexOf(field) == -1) { diff --git a/util/resend.js b/util/resend.js index 4b18025..72a9831 100644 --- a/util/resend.js +++ b/util/resend.js @@ -1,8 +1,7 @@ "use strict"; const config = require("config"), - crypto = require("crypto"), - hb = require("handlebars"); + { sendVerifyMail } = require("../server/lib/mail"); const basePath = "/" + config.get("basePath").replace(/^\/+/, "").replace(/\/+$/, "") + "/"; @@ -36,128 +35,32 @@ if (config.has("admin.mail") && console.log("SMTP disabled. To enable SMTP, configure admin.mail, smtp.host, and smtp.sender"); } -const templates = { - "html": [ - "

Hello {{username}},

", - "", - "

Welcome to ketrenos.com. You are almost done creating your account. ", - "Before you can access the system, you must verify your email address.

", - "", - "

To do so, simply access this link:

", - "

VERIFY {{mail}} ADDRESS

", - "", - "

Sincerely,

", - "

James

" - ].join("\n"), - "text": [ - "Hello {{username}},", - "", - "Welcome to ketrenos.com. You are almost done creating your account. ", - "Before you can access the system, you must verify your email address.", - "", - "To do so, simply access this link:", - "", - "{{url}}{{secret}}", - "", - "Sincerely,", - "James" - ].join("\n") -}; +if (process.argv.length != 3) { + console.log("usage: node resend UID"); + process.exit(-1); +} require("../server/db/users").then(function(db) { - const userDB = db; - let user = null; + let id = parseInt(process.argv[2]); - return userDB.sequelize.query("SELECT * FROM users WHERE id=:id", { + return db.sequelize.query("SELECT * FROM users WHERE id=:id", { replacements: { - id: process.argv[2] + id: id }, - type: userDB.Sequelize.QueryTypes.SELECT, + type: db.Sequelize.QueryTypes.SELECT, raw: true - }).then(function(results) { - if (results.length != 1) { - return ("User not found."); + }).then(function(users) { + if (users.length != 1) { + console.log("User not found: " + id); + process.exit(-1); } - - user = results[0]; + return users[0]; + }).then(function(user) { + return sendVerifyMail(db, req, user); }).then(function() { - return userDB.sequelize.query("DELETE FROM authentications WHERE userId=:id", { - replacements: { - id: user.id - } - }); - }).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 authentications " + - "(userId,issued,key,type) VALUES " + - "(:userId,CURRENT_TIMESTAMP,:key,'account-setup')", { - replacements: { - key: secret, - userId: user.id - } - }).then(function() { - return secret; - }).catch(function(error) { - console.log(error); - throw error; - }); - }).then(function(secret) { - const transporter = req.app.get("transporter"); - if (!transporter) { - console.log("Not sending VERIFY email; SMTP not configured."); - return; - } - - let data = { - username: user.displayName, - mail: user.mail, - secret: secret, - url: req.protocol + "://" + req.hostname + req.app.get("basePath") - }, envelope = { - to: data.mail, - from: config.get("smtp.sender"), - subject: "Request to ketrenos.com create account for '" + data.username + "'", - 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() { - process.exit(0); - }); + process.exit(0); }).catch(function(error) { - console.log("Error creating account: ", error); + console.log("Error sending verification mail: ", error); }); });