"use strict"; process.env.TZ = "Etc/GMT"; 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(/\/$/, "") + "/", facesPath = config.get("facesPath").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(`Loading faces out of: ${facesPath} (mapped to ${basePath}faces})`); 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); app.use(basePath, require("./routes/basepath.js")); /* Handle static files first so excessive logging doesn't occur */ app.use(basePath, express.static("../frontend", { index: false })); app.use(basePath + "dist", express.static("../dist", { 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(); }); }); let dbPath = config.get("sessions.db"); let configPath = process.env.NODE_CONFIG_DIR; if (configPath) { configPath = configPath.replace(/config/, ''); } else { configPath = './' } dbPath = `${configPath}${dbPath}`; app.use(session({ store: new SQLiteStore({ db: dbPath }), secret: config.get("sessions.store-secret"), cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 }, // 1 week saveUninitialized: false, resave: true })); 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": [ "

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 db/users.db

", "", "

Sincerely,
", "James

" ].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 db/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((err, req, res, next) => { res.status(err.status || 500).json({ message: err.message, error: {} }); }); /* Check authentication */ app.use(basePath, (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); }); }); app.use(`${basePath}faces/`, express.static(facesPath, { maxAge: '14d', index: false })); /* Everything below here requires a successful authentication */ app.use(basePath, express.static(picturesPath, { maxAge: '14d', 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/holidays", require("./routes/holidays")); app.use(basePath + "api/v1/faces", require("./routes/faces")); app.use(basePath + "api/v1/scan", require("./routes/scan")(scanner)); app.use(basePath + "api/v1/identities", require("./routes/identities")); /* 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); });