diff --git a/server/app.js b/server/app.js index 5fb7811..8781c2d 100755 --- a/server/app.js +++ b/server/app.js @@ -13,6 +13,7 @@ const express = require("express"), bodyParser = require("body-parser"), config = require("config"), session = require('express-session'), + hb = require("handlebars"), SQLiteStore = require('connect-sqlite3')(session), scanner = require("./scanner"); @@ -21,8 +22,14 @@ require("./console-line.js"); /* Monkey-patch console.log with line numbers */ const picturesPath = config.get("picturesPath").replace(/\/$/, ""), serverConfig = config.get("server"); +config.get("admin.mail"); +config.get("smtp.host"); +config.get("smtp.sender"); + let basePath = config.get("basePath"); +let photoDB = null, userDB = null + basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/"; if (basePath == "//") { basePath = "/"; @@ -70,7 +77,123 @@ app.use(session({ 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": [ + "
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
Dear {{name}},
", + "Dear {{username}},
", "", - "Welcome to HTML {{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: {{secret}}
", "", "Sincerely,
", "James
" ].join("\n"), "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,", "James" @@ -75,7 +79,7 @@ function ldapPromise(username, password) { router.post("/create", function(req, res) { let who = req.query.w || req.body.w || "", 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; if (!who || !password || !mail || !name) { @@ -93,13 +97,21 @@ router.post("/create", function(req, res) { 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,}))$/, - secret = "magic cookie"; + 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); 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 " + "(uid,displayName,password,mail,memberSince,authenticated,notes) " + "VALUES(:username,:name,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", { @@ -122,17 +134,18 @@ router.post("/create", function(req, res) { console.log(error); throw error; }); - }).spread(function(results, metadata) { + }).then(function() { let data = { username: name, mail: mail, - secret: secret + secret: secret, + url: req.protocol + "://" + req.hostname + req.app.get("basePath") }, envelope = { to: mail, from: config.get("smtp.sender"), - subject: "Request to create account for " + name, + subject: "Request to ketrenos.com create account for '" + name + "'", cc: "", - bcc: "", + bcc: config.get("admin.mail"), text: hb.compile(templates.text)(data), html: hb.compile(templates.html)(data) }; @@ -208,7 +221,7 @@ router.post("/login", function(req, res) { }); }).then(function(user) { if (!user) { - console.log(username + " not found."); + console.log(username + " not found (or invalid password.)"); req.session.user = {}; return res.status(401).send("Invalid login credentials"); } diff --git a/server/scanner.js b/server/scanner.js index 678595b..d75bde4 100755 --- a/server/scanner.js +++ b/server/scanner.js @@ -11,7 +11,7 @@ let photoDB = null; 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" ]; @@ -574,7 +574,8 @@ function findOrCreateDBAlbum(transaction, album) { } 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) { let error = "Asset being processed without an album"; console.error(error); @@ -651,111 +652,106 @@ function doScan() { 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) { - console.log("Found " + assets.length + " assets in " + albums.length + " albums after " + - ((Date.now() - now) / 1000) + "s"); - /* One at a time, in series, as the album[] array has parents first, then descendants. - * Operating in parallel could result in a child being searched for prior to the parent */ - now = Date.now(); + return scanDir(null, picturesPath).spread(function(albums, assets) { + console.log("Found " + assets.length + " assets in " + albums.length + " albums after " + + ((Date.now() - now) / 1000) + "s"); + /* One at a time, in series, as the album[] array has parents first, then descendants. + * Operating in parallel could result in a child being searched for prior to the parent */ + now = Date.now(); - let toProcess = albums.length, lastMessage = moment(); - return photoDB.sequelize.transaction(function(transaction) { - return Promise.mapSeries(albums, function(album) { - return findOrCreateDBAlbum(transaction, album).then(function() { - toProcess--; - if (moment().add(-5, 'seconds') > lastMessage) { - console.log("Albums to be created in DB: " + toProcess); - lastMessage = moment(); - } - }); - }); - }).then(function() { - console.log("Processed " + albums.length + " album DB entries in " + - ((Date.now() - now) / 1000) + "s"); - now = Date.now(); - - console.log(assets.length + " assets remaining to be verified/updated. ETA N/A"); - - let processed = 0, start = Date.now(), last = 0, updateScanned = [], newEntries = 0; - return photoDB.sequelize.transaction(function(transaction) { - return Promise.map(assets, function(asset) { - return new Promise(function(resolve, reject) { - /* If both mtime and ctime of the asset are older than the lastScan, - * skip this asset. */ - if (lastScan != null && asset.stats.mtime < lastScan && asset.stats.ctime < lastScan) { - updateScanned.push(asset.id); - return resolve(asset); - } - - return findOrUpdateDBAsset(transaction, asset).then(function(asset) { - if (!asset.scanned) { - newEntries++; - } - console.log(typeof asset.scanned); - if (!asset.scanned || asset.scanned < asset.stats.mtime || !asset.modified) { - console.log("Process asset: " + asset.scanned); - needsProcessing.push(asset); - } else { - console.log("Update scanned tag: " + asset.scanned); - updateScanned.push(asset.id); - } - return asset; - }).then(function(asset) { - return resolve(asset); - }).catch(function(error) { - return reject(error); - }); - }).then(function(asset) { - processed++; - - let elapsed = Date.now() - start; - if (elapsed < 5000) { - return asset; - } - - let remaining = assets.length - processed, eta = Math.ceil((elapsed / 1000) * remaining / (processed - last)); - console.log(remaining + " assets remaining be verified/updated " + - "(" + newEntries + " new entries, " + (processed - newEntries) + " up-to-date so far). ETA " + eta + "s"); - last = processed; - start = Date.now(); - }); - }, { - concurrency: 10 - }); - }).then(function() { - if (updateScanned.length) { - return photoDB.sequelize.query("UPDATE photos SET scanned=CURRENT_TIMESTAMP WHERE id IN (:ids)", { - replacements: { - ids: updateScanned - } - }).then(function() { - console.log("Updated scan date of " + updateScanned.length + " assets"); - updateScanned = []; - }); + let toProcess = albums.length, lastMessage = moment(); + return photoDB.sequelize.transaction(function(transaction) { + return Promise.mapSeries(albums, function(album) { + return findOrCreateDBAlbum(transaction, album).then(function() { + toProcess--; + if (moment().add(-5, 'seconds') > lastMessage) { + console.log("Albums to be created in DB: " + toProcess); + lastMessage = moment(); } - }).then(function() { - console.log(newEntries + " assets are new. " + (needsProcessing.length - newEntries) + " assets have been modified."); - console.log(needsProcessing.length + " assets need HASH computed. " + (assets.length - needsProcessing.length) + " need no update.");; - processBlock(needsProcessing); - needsProcessing = []; - }).then(function() { - console.log("Scanned " + assets.length + " asset DB entries in " + - ((Date.now() - now) / 1000) + "s"); - assets = []; }); }); }).then(function() { - console.log("Total time to initialize DB and all scans: " + ((Date.now() - initialized) / 1000) + "s"); + console.log("Processed " + albums.length + " album DB entries in " + + ((Date.now() - now) / 1000) + "s"); + now = Date.now(); + + console.log(assets.length + " assets remaining to be verified/updated. ETA N/A"); + + let processed = 0, start = Date.now(), last = 0, updateScanned = [], newEntries = 0; + return photoDB.sequelize.transaction(function(transaction) { + return Promise.map(assets, function(asset) { + return new Promise(function(resolve, reject) { + /* If both mtime and ctime of the asset are older than the lastScan, skip it + * Can only do this after a full scan has occurred */ + if (lastScan != null && asset.stats.mtime < lastScan && asset.stats.ctime < lastScan) { + return resolve(asset); + } + + return findOrUpdateDBAsset(transaction, asset).then(function(asset) { + if (!asset.scanned) { + newEntries++; + } + if (!asset.scanned || asset.scanned < asset.stats.mtime || !asset.modified) { + needsProcessing.push(asset); + } else { + updateScanned.push(asset.id); + } + return asset; + }).then(function(asset) { + return resolve(asset); + }).catch(function(error) { + return reject(error); + }); + }).then(function(asset) { + processed++; + + let elapsed = Date.now() - start; + if (elapsed < 5000) { + return asset; + } + + let remaining = assets.length - processed, eta = Math.ceil((elapsed / 1000) * remaining / (processed - last)); + console.log(remaining + " assets remaining be verified/updated " + + "(" + newEntries + " new entries, " + (processed - newEntries) + " up-to-date so far). ETA " + eta + "s"); + last = processed; + start = Date.now(); + }); + }, { + concurrency: 10 + }); + }).then(function() { + if (updateScanned.length) { + return photoDB.sequelize.query("UPDATE photos SET scanned=CURRENT_TIMESTAMP WHERE id IN (:ids)", { + replacements: { + ids: updateScanned + } + }).then(function() { + console.log("Updated scan date of " + updateScanned.length + " assets"); + updateScanned = []; + }); + } + }).then(function() { + console.log(newEntries + " assets are new. " + (needsProcessing.length - newEntries) + " assets have been modified."); + console.log(needsProcessing.length + " assets need HASH computed. " + (assets.length - needsProcessing.length) + " need no update.");; + processBlock(needsProcessing); + needsProcessing = []; + }).then(function() { + console.log("Scanned " + assets.length + " asset DB entries in " + + ((Date.now() - now) / 1000) + "s"); + assets = []; + }); + }); + }).then(function() { + 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() { scanning = false;