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 <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2018-10-17 22:53:39 -07:00
parent 2aeb733b2c
commit 814bac8b78
5 changed files with 192 additions and 252 deletions

View File

@ -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.";
}
},

View File

@ -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);
});
});

116
server/lib/mail.js Normal file
View File

@ -0,0 +1,116 @@
"use strict";
const config = require("config"),
crypto = require("crypto"),
hb = require("handlebars");
const templates = {
"html": [
"<p>Hello {{username}},</p>",
"",
"<p>Welcome to <b>ketrenos.com</b>. You are almost done creating your account. ",
"Before you can access the system, you must verify your email address.</p>",
"",
"<p>To do so, simply access this link:</p>",
"<p><a href=\"{{url}}{{secret}}\">VERIFY {{mail}} ADDRESS</a></p>",
"",
"<p>Sincerely,</p>",
"<p>James</p>"
].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
}

View File

@ -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": [
"<p>Hello {{username}},</p>",
"",
"<p>Welcome to <b>ketrenos.com</b>. You are almost done creating your account. ",
"Before you can access the system, you must verify your email address.</p>",
"",
"<p>To do so, simply access this link:</p>",
"<p><a href=\"{{url}}{{secret}}\">VERIFY {{mail}} ADDRESS</a></p>",
"",
"<p>Sincerely,</p>",
"<p>James</p>"
].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) {

View File

@ -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": [
"<p>Hello {{username}},</p>",
"",
"<p>Welcome to <b>ketrenos.com</b>. You are almost done creating your account. ",
"Before you can access the system, you must verify your email address.</p>",
"",
"<p>To do so, simply access this link:</p>",
"<p><a href=\"{{url}}{{secret}}\">VERIFY {{mail}} ADDRESS</a></p>",
"",
"<p>Sincerely,</p>",
"<p>James</p>"
].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);
});
});