Account creation technically plumpbed; UX isn't the best when setting up account

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2018-10-01 19:09:02 -07:00
parent 1dc3d74743
commit 389185a59c
4 changed files with 258 additions and 120 deletions

View File

@ -13,6 +13,7 @@ const express = require("express"),
bodyParser = require("body-parser"), bodyParser = require("body-parser"),
config = require("config"), config = require("config"),
session = require('express-session'), session = require('express-session'),
hb = require("handlebars"),
SQLiteStore = require('connect-sqlite3')(session), SQLiteStore = require('connect-sqlite3')(session),
scanner = require("./scanner"); scanner = require("./scanner");
@ -21,8 +22,14 @@ require("./console-line.js"); /* Monkey-patch console.log with line numbers */
const picturesPath = config.get("picturesPath").replace(/\/$/, ""), const picturesPath = config.get("picturesPath").replace(/\/$/, ""),
serverConfig = config.get("server"); serverConfig = config.get("server");
config.get("admin.mail");
config.get("smtp.host");
config.get("smtp.sender");
let basePath = config.get("basePath"); let basePath = config.get("basePath");
let photoDB = null, userDB = null
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/"; basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
if (basePath == "//") { if (basePath == "//") {
basePath = "/"; basePath = "/";
@ -70,7 +77,123 @@ app.use(session({
const index = require("./routes/index"); const index = require("./routes/index");
const transporter = require("nodemailer").createTransport({
host: config.get("smtp.host"),
pool: true,
port: config.has("smtp.port") ? config.get("smtp.port") : 25
});
const templates = {
"html": [
"<p>The user {{displayName}} has verified their email address ({{mail}}).</p>",
"",
"<p>They indicated they know:</p>",
"<pre>{{notes}}</pre>",
"",
"<p>To authenticate:</p>",
"<p>echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 users.db</p>",
"",
"<p>Sincerely,<br>",
"James</p>"
].join("\n"),
"text": [
"The user {{displayName}} has verified their email address ({{mail}}).",
"",
"They indicated they know:",
"{{notes}}",
"",
"To authenticate:",
"echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 users.db",
"",
"Sincerely,",
"James"
].join("\n")
};
/* Allow loading of the app w/out being logged in */ /* Allow loading of the app w/out being logged in */
app.use(basePath, function(req, res, next) {
let match = req.url.match(/^\/([0-9a-f]+)$/);
if (!match) {
return next();
}
let key = match[1];
return userDB.sequelize.query("SELECT * FROM authentications WHERE key=:key", {
replacements: {
key: key
},
type: userDB.sequelize.QueryTypes.SELECT
}).then(function(results) {
let token;
if (results.length == 0) {
console.log("Invalid key. Ignoring.");
return next();
}
token = results[0];
console.log("Matched token: " + JSON.stringify(token, null, 2));
switch (token.type) {
case "account-setup":
return userDB.sequelize.query("UPDATE users SET mailVerified=1 WHERE id=:userId", {
replacements: token
}).then(function() {
return userDB.sequelize.query("DELETE FROM authentications WHERE key=:key", {
replacements: token
});
}).then(function() {
return userDB.sequelize.query("SELECT * FROM users WHERE id=:userId", {
replacements: token,
type: userDB.sequelize.QueryTypes.SELECT
}).then(function(results) {
if (results.length == 0) {
throw "DB mis-match between authentications and users table";
}
let user = results[0],
envelope = {
to: config.get("admin.mail"),
from: config.get("smtp.sender"),
subject: "VERIFIED: Account'" + user.displayName + "'",
cc: "",
bcc: "",
text: hb.compile(templates.text)(user),
html: hb.compile(templates.html)(user)
};
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 res.redirect(308, basePath);
});
});
}
return next();
});
});
app.use(basePath, index); app.use(basePath, index);
app.use(basePath + "api/v1/users", require("./routes/users")); app.use(basePath + "api/v1/users", require("./routes/users"));
@ -125,11 +248,16 @@ app.set("port", serverConfig.port);
const server = require("http").createServer(app); const server = require("http").createServer(app);
require("./db/photos").then(function(photoDB) { require("./db/photos").then(function(db) {
photoDB = db;
}).then(function() {
return require("./db/users").then(function(db) {
userDB = db;
});
}).then(function() {
console.log("DB connected. Opening server."); console.log("DB connected. Opening server.");
server.listen(serverConfig.port); server.listen(serverConfig.port);
return photoDB; }).then(function() {
}).then(function(photoDB) {
console.log("Scanning."); console.log("Scanning.");
scanner.init(photoDB); scanner.init(photoDB);
return scanner.scan(); return scanner.scan();

View File

@ -38,6 +38,7 @@ function init() {
authToken: Sequelize.STRING, authToken: Sequelize.STRING,
authDate: Sequelize.DATE, authDate: Sequelize.DATE,
authenticated: Sequelize.BOOLEAN, authenticated: Sequelize.BOOLEAN,
mailVerified: Sequelize.BOOLEAN,
mail: Sequelize.STRING, mail: Sequelize.STRING,
memberSince: Sequelize.DATE, memberSince: Sequelize.DATE,
password: Sequelize.STRING, /* SHA hash of user supplied password for !isLDAP users */ password: Sequelize.STRING, /* SHA hash of user supplied password for !isLDAP users */

View File

@ -37,21 +37,25 @@ router.get("/", function(req, res/*, next*/) {
const templates = { const templates = {
"html": [ "html": [
"<p>Dear {{name}},</p>", "<p>Dear {{username}},</p>",
"", "",
"<p>Welcome to HTML {{username}}.</p>", "<p>Welcome to <b>ketrenos.com</b>. Before you can access the system, you must authenticate",
"your email address ({{mail}}).</p>",
"", "",
"<p>Your secret is: <b>{{secret}}</b>.</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>Sincerely,</p>",
"<p>James</p>" "<p>James</p>"
].join("\n"), ].join("\n"),
"text": [ "text": [
"Dear {{name}},", "Dear {{username}},",
"", "",
"Welcome to TEXT {{username}}.", "Welcome to ketrenos.com. Before you can access the system, you must authenticate",
"your email address ({{mail}}).",
"", "",
"Your secret is: {{secret}}.", "To do so, simply access this link, which contains your authentication ",
"token: {{url}}{{secret}}",
"", "",
"Sincerely,", "Sincerely,",
"James" "James"
@ -75,7 +79,7 @@ function ldapPromise(username, password) {
router.post("/create", function(req, res) { router.post("/create", function(req, res) {
let who = req.query.w || req.body.w || "", let who = req.query.w || req.body.w || "",
password = req.query.p || req.body.p || "", password = req.query.p || req.body.p || "",
name = req.query.n || req.body.n || username, name = req.query.n || req.body.n || "",
mail = req.query.m || req.body.m; mail = req.query.m || req.body.m;
if (!who || !password || !mail || !name) { if (!who || !password || !mail || !name) {
@ -93,13 +97,21 @@ router.post("/create", function(req, res) {
return res.status(400).send("Email address already used."); 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,}))$/, 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,}))$/;
secret = "magic cookie";
if (!re.exec(mail)) { if (!re.exec(mail)) {
console.log("Invalid email address: " + mail); console.log("Invalid email address: " + mail);
return res.status(400).send("Invalid email address."); 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 " + return userDB.sequelize.query("INSERT INTO users " +
"(uid,displayName,password,mail,memberSince,authenticated,notes) " + "(uid,displayName,password,mail,memberSince,authenticated,notes) " +
"VALUES(:username,:name,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", { "VALUES(:username,:name,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", {
@ -122,17 +134,18 @@ router.post("/create", function(req, res) {
console.log(error); console.log(error);
throw error; throw error;
}); });
}).spread(function(results, metadata) { }).then(function() {
let data = { let data = {
username: name, username: name,
mail: mail, mail: mail,
secret: secret secret: secret,
url: req.protocol + "://" + req.hostname + req.app.get("basePath")
}, envelope = { }, envelope = {
to: mail, to: mail,
from: config.get("smtp.sender"), from: config.get("smtp.sender"),
subject: "Request to create account for " + name, subject: "Request to ketrenos.com create account for '" + name + "'",
cc: "", cc: "",
bcc: "", bcc: config.get("admin.mail"),
text: hb.compile(templates.text)(data), text: hb.compile(templates.text)(data),
html: hb.compile(templates.html)(data) html: hb.compile(templates.html)(data)
}; };
@ -208,7 +221,7 @@ router.post("/login", function(req, res) {
}); });
}).then(function(user) { }).then(function(user) {
if (!user) { if (!user) {
console.log(username + " not found."); console.log(username + " not found (or invalid password.)");
req.session.user = {}; req.session.user = {};
return res.status(401).send("Invalid login credentials"); return res.status(401).send("Invalid login credentials");
} }

View File

@ -11,7 +11,7 @@ let photoDB = null;
const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/"; const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/";
let processQueue = [], triedClean = [], lastScan = new Date("1900-01-01"); let processQueue = [], triedClean = [], lastScan = new Date("1800-01-01");
//const rawExtension = /\.(nef|orf)$/i, extensions = [ "jpg", "jpeg", "png", "gif", "nef", "orf" ]; //const rawExtension = /\.(nef|orf)$/i, extensions = [ "jpg", "jpeg", "png", "gif", "nef", "orf" ];
@ -574,7 +574,8 @@ function findOrCreateDBAlbum(transaction, album) {
} }
function findOrUpdateDBAsset(transaction, asset) { function findOrUpdateDBAsset(transaction, asset) {
let query = "SELECT id,DATETIME(scanned) AS scanned,DATETIME(modified) AS modified FROM photos WHERE albumId=:albumId AND filename=:filename"; let query = "SELECT id,DATETIME(scanned) AS scanned,DATETIME(modified) AS modified FROM photos " +
"WHERE albumId=:albumId AND filename=:filename";
if (!asset.album || !asset.album.id) { if (!asset.album || !asset.album.id) {
let error = "Asset being processed without an album"; let error = "Asset being processed without an album";
console.error(error); console.error(error);
@ -651,16 +652,6 @@ function doScan() {
return Promise.resolve("scanning"); return Promise.resolve("scanning");
} }
return photoDB.sequelize.query("SELECT max(scanned) AS scanned FROM photos", {
type: photoDB.sequelize.QueryTypes.SELECT
}).then(function(results) {
if (results[0].scanned == null) {
lastScan = new Date("1800-01-01");
} else {
lastScan = new Date(results[0].scanned);
}
console.log("Updating any asset newer than " + moment(lastScan).format());
}).then(function() {
return scanDir(null, picturesPath).spread(function(albums, assets) { return scanDir(null, picturesPath).spread(function(albums, assets) {
console.log("Found " + assets.length + " assets in " + albums.length + " albums after " + console.log("Found " + assets.length + " assets in " + albums.length + " albums after " +
((Date.now() - now) / 1000) + "s"); ((Date.now() - now) / 1000) + "s");
@ -690,10 +681,9 @@ function doScan() {
return photoDB.sequelize.transaction(function(transaction) { return photoDB.sequelize.transaction(function(transaction) {
return Promise.map(assets, function(asset) { return Promise.map(assets, function(asset) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
/* If both mtime and ctime of the asset are older than the lastScan, /* If both mtime and ctime of the asset are older than the lastScan, skip it
* skip this asset. */ * Can only do this after a full scan has occurred */
if (lastScan != null && asset.stats.mtime < lastScan && asset.stats.ctime < lastScan) { if (lastScan != null && asset.stats.mtime < lastScan && asset.stats.ctime < lastScan) {
updateScanned.push(asset.id);
return resolve(asset); return resolve(asset);
} }
@ -701,12 +691,9 @@ function doScan() {
if (!asset.scanned) { if (!asset.scanned) {
newEntries++; newEntries++;
} }
console.log(typeof asset.scanned);
if (!asset.scanned || asset.scanned < asset.stats.mtime || !asset.modified) { if (!asset.scanned || asset.scanned < asset.stats.mtime || !asset.modified) {
console.log("Process asset: " + asset.scanned);
needsProcessing.push(asset); needsProcessing.push(asset);
} else { } else {
console.log("Update scanned tag: " + asset.scanned);
updateScanned.push(asset.id); updateScanned.push(asset.id);
} }
return asset; return asset;
@ -756,6 +743,15 @@ function doScan() {
}); });
}).then(function() { }).then(function() {
console.log("Total time to initialize DB and all scans: " + ((Date.now() - initialized) / 1000) + "s"); console.log("Total time to initialize DB and all scans: " + ((Date.now() - initialized) / 1000) + "s");
return photoDB.sequelize.query("SELECT max(scanned) AS scanned FROM photos", {
type: photoDB.sequelize.QueryTypes.SELECT
}).then(function(results) {
if (results[0].scanned == null) {
lastScan = new Date("1800-01-01");
} else {
lastScan = new Date(results[0].scanned);
}
console.log("Updating any asset newer than " + moment(lastScan).format());
}); });
}).then(function() { }).then(function() {
scanning = false; scanning = false;