James Ketrenos 814bac8b78 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>
2018-10-17 22:53:39 -07:00

321 lines
8.9 KiB
JavaScript
Executable File

"use strict";
process.env.TZ = "Etc/GMT";
if (process.env.LOG_LINE) {
require("./monkey.js"); /* monkey patch console.log */
}
console.log("Loading photos.ketr");
const express = require("express"),
morgan = require("morgan"),
bodyParser = require("body-parser"),
config = require("config"),
session = require('express-session'),
hb = require("handlebars"),
SQLiteStore = require('connect-sqlite3')(session),
scanner = require("./scanner");
require("./console-line.js"); /* Monkey-patch console.log with line numbers */
const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/",
serverConfig = config.get("server");
let basePath = config.get("basePath");
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
if (basePath == "//") {
basePath = "/";
}
let photoDB = null, userDB = null;
console.log("Loading pictures out of: " + picturesPath);
console.log("Hosting server from: " + basePath);
const app = express();
app.set("basePath", basePath);
/* App is behind an nginx proxy which we trust, so use the remote address
* set in the headers */
app.set("trust proxy", true);
/* Handle static files first so excessive logging doesn't occur */
app.use(basePath, express.static("frontend", { index: false }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: false
}));
/* *******************************************************************************
* Logging - begin
*
* This runs before after cookie parsing, but before routes. If we set
* immediate: true on the morgan options, it happens before cookie parsing
* */
morgan.token('remote-user', function (req) {
return req.user ? req.user.username : "N/A";
});
/* Any path starting with the following won't be logged via morgan */
const logSkipPaths = new RegExp("^" + basePath + "(" + [
".*thumbs\\/",
"bower_components",
].join(")|(") + ")");
app.use(morgan('common', {
skip: function (req) {
return logSkipPaths.exec(req.originalUrl);
}
}));
/*
* Logging - end
* ******************************************************************************* */
/* body-parser does not support text/*, so add support for that here */
app.use(function(req, res, next){
if (!req.is('text/*')) {
return next();
}
req.setEncoding('utf8');
let text = '';
req.on('data', function(chunk) {
text += chunk;
});
req.on('end', function() {
req.text = text;
next();
});
});
app.use(session({
store: new SQLiteStore({ db: config.get("sessions.db") }),
secret: config.get("sessions.store-secret"),
cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 1 week
}));
const index = require("./routes/index");
if (config.has("admin.mail") &&
config.has("smtp.host") &&
config.has("smtp.sender")) {
app.set("transporter", require("nodemailer").createTransport({
host: config.get("smtp.host"),
pool: true,
port: config.has("smtp.port") ? config.get("smtp.port") : 25
}));
} else {
console.log("SMTP disabled. To enable SMTP, configure admin.mail, smtp.host, and smtp.sender");
}
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")
};
/* Look for action-token URLs and process; this does not require a user to be 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";
}
const transporter = app.get("transporter");
if (!transporter) {
console.log("Not sending VERIFIED email; SMTP not configured.");
return;
}
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)
};
req.session.userId = user.id;
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();
});
});
/* Allow loading of the app w/out being logged in */
app.use(basePath, index);
/* Allow access to the 'users' API w/out being logged in */
const users = require("./routes/users");
app.use(basePath + "api/v1/users", users.router);
app.use(function(err, req, res, next) {
res.status(err.status || 500).json({
message: err.message,
error: {}
});
});
/* 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(403).send(error);
});
});
/* Everything below here requires a successful authentication */
app.use(basePath, express.static(picturesPath, { index: false }));
app.use(basePath + "api/v1/photos", require("./routes/photos"));
app.use(basePath + "api/v1/days", require("./routes/days"));
app.use(basePath + "api/v1/albums", require("./routes/albums"));
app.use(basePath + "api/v1/scan", require("./routes/scan")(scanner));
/* Declare the "catch all" index route last; the final route is a 404 dynamic router */
app.use(basePath + "/*", index);
/**
* Create HTTP server and listen for new connections
*/
app.set("port", serverConfig.port);
const server = require("http").createServer(app);
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.");
server.listen(serverConfig.port);
}).then(function() {
console.log("Scanning.");
scanner.init(photoDB);
return scanner.scan();
}).then(function() {
console.log("Scanning completed.");
}).catch(function(error) {
console.error(error);
process.exit(-1);
});
server.on("error", function(error) {
if (error.syscall !== "listen") {
throw error;
}
// handle specific listen errors with friendly messages
switch (error.code) {
case "EACCES":
console.error(serverConfig.port + " requires elevated privileges");
process.exit(1);
break;
case "EADDRINUSE":
console.error(serverConfig.port + " is already in use");
process.exit(1);
break;
default:
throw error;
}
});
server.on("listening", function() {
console.log("Listening on " + serverConfig.port);
});