ketrenos.com is a personal website for my family and friends.
+
If you already have an email account on this domain, you can login to the photo viewer
+ using your normal @ketrenos.com account name, and password.
+ If you are a friend or family member (immediate, or extended)
+ request access,
+ provide your email address, and tell me who in the extended Ketrenos
+ universe you know. If you're not a bot, I'll very likely give you access :)
+
+
+
+ login
+
+
+
-
-
-
~ the end ~
-
pager
@@ -323,6 +369,16 @@
item
+
+
+
Hello!
+
+ Unfortunately, I haven't built this part of the site yet... send me an email (james @ ketrenos.com)
+ and I'll create an account for you.
+
+ close
+
+
@@ -333,10 +389,22 @@
Polymer({
is: "ketr-photos",
properties: {
+ password: {
+ type: String,
+ value: ""
+ },
+ username: {
+ type: String,
+ value: ""
+ },
years: {
type: Array,
value: []
},
+ user: {
+ type: Object,
+ value: null
+ },
order: {
type: String,
value: "by-date"
@@ -373,7 +441,7 @@
},
mode: {
type: String,
- value: "memories"
+ value: "login"
},
date: {
type: String,
@@ -387,6 +455,32 @@
"dateChanged(date)"
],
+ disableLogin: function(username, password) {
+ return !username || username == "" || !password || password == "";
+ },
+
+ enterCheck: function(event) {
+ if (event.code == 'Enter') {
+ if (event.currentTarget.id == "username") {
+ event.preventDefault();
+ this.async(function() {
+ this.$.password.focus();
+ }, 100);
+ return;
+ }
+
+ if (event.currentTarget.id == "password") {
+ event.preventDefault();
+ this.login();
+ return;
+ }
+ }
+ },
+
+ requestAccess: function(event) {
+ this.$.requestAccess.open();
+ },
+
add: function(a, b) {
return parseInt(a) + parseInt(b);
},
@@ -407,6 +501,39 @@
return order == "by-album";
},
+ login: function(event) {
+ if (this.loading) {
+ return;
+ }
+ this.loading = true;
+ window.fetch("api/v1/users/login", function(error, xhr) {
+ this.loading = false;
+ this.password = "";
+ var user;
+ try {
+ user = JSON.parse(xhr.responseText);
+ } catch(___) {
+ this.$.toast.text = "Unable to load/parse user information.";
+ this.$.toast.setAttribute("error", true);
+ this.$.toast.updateStyles();
+ this.$.toast.show();
+ console.error("Unable to parse user information");
+ }
+ this.user = user;
+ }.bind(this), null, "POST", { u: this.username, p: this.password });
+ },
+
+ logout: function(event) {
+ if (this.loading) {
+ return;
+ }
+ this.loading = true;
+ window.fetch("api/v1/users/logout", function(error, xhr) {
+ this.loading = false;
+ this.user = null;
+ }.bind(this));
+ },
+
changeMode: function(event) {
var mode = event.currentTarget.icon;
if (this.mode != mode) {
@@ -1001,6 +1128,14 @@
}, 100);
},
+ userChanged: function(user) {
+ this.resetPhotos();
+ if (user) {
+ this._loadAlbums();
+ this._loadPhotos();
+ }
+ },
+
ready: function() {
this.$.calendar.partsHidden = {
"year": true
@@ -1017,8 +1152,31 @@
}
}.bind(this), 100);
- this._loadAlbums();
- this._loadPhotos();
+ this.loading = true;
+ window.fetch("api/v1/users", function(error, xhr) {
+ this.loading = false;
+
+ if (error) {
+ console.error(JSON.stringify(error, null, 2));
+ return;
+ }
+
+ var results;
+ try {
+ results = JSON.parse(xhr.responseText);
+ } catch (___) {
+ this.$.toast.text = "Unable to parse authentication response.";
+ this.$.toast.setAttribute("error", true);
+ this.$.toast.updateStyles();
+ this.$.toast.show();
+ return;
+ }
+
+ if (results && results.username) {
+ this.user = results;
+ this.mode = "memories";
+ }
+ }.bind(this));
this.onResize();
diff --git a/package.json b/package.json
index ff2c2bc..7ef490d 100644
--- a/package.json
+++ b/package.json
@@ -16,9 +16,12 @@
"bluebird": "^3.5.1",
"body-parser": "^1.18.2",
"config": "^1.28.1",
+ "connect-sqlite3": "^0.9.11",
"cookie-parser": "^1.4.3",
"exif-reader": "github:paras20xx/exif-reader",
"express": "^4.16.2",
+ "express-session": "^1.15.6",
+ "ldapauth-fork": "^4.0.2",
"mariasql": "^0.2.6",
"moment": "^2.22.2",
"morgan": "^1.9.0",
diff --git a/server/app.js b/server/app.js
index 5b0f82a..90f21f3 100755
--- a/server/app.js
+++ b/server/app.js
@@ -12,7 +12,8 @@ const express = require("express"),
morgan = require("morgan"),
bodyParser = require("body-parser"),
config = require("config"),
- db = require("./db"),
+ session = require('express-session'),
+ SQLiteStore = require('connect-sqlite3')(session),
scanner = require("./scanner");
require("./console-line.js"); /* Monkey-patch console.log with line numbers */
@@ -40,10 +41,9 @@ 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(basePath, express.static(picturesPath, { index: false }));
-
app.use(morgan("common"));
+app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: false
}));
@@ -64,12 +64,18 @@ app.use(function(req, res, next){
});
});
-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(session({
+ store: new SQLiteStore({ db: config.get("sessions.db") }),
+ secret: config.get("sessions.store-secret"),
+ cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 1 week
+}));
-/* Declare the "catch all" index route last; the final route is a 404 dynamic router */
-app.use(basePath, require("./routes/index"));
+const index = require("./routes/index");
+
+/* Allow loading of the app w/out being logged in */
+app.use(basePath, index);
+
+app.use(basePath + "api/v1/users", require("./routes/users"));
app.use(function(err, req, res, next) {
res.status(err.status || 500).json({
@@ -78,6 +84,23 @@ app.use(function(err, req, res, next) {
});
});
+/* Everything below here requires a successful authentication */
+
+app.use(basePath, function(req, res, next) {
+ if (!req.session || !req.session.user || !req.session.user.username) {
+ return res.status(401).send("Unauthorized");
+ }
+ return next();
+});
+
+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"));
+
+/* 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
*/
@@ -85,7 +108,7 @@ app.set("port", serverConfig.port);
const server = require("http").createServer(app);
-db.then(function(photoDB) {
+require("./db/photos").then(function(photoDB) {
console.log("DB connected. Opening server.");
server.listen(serverConfig.port);
return photoDB;
diff --git a/server/db/index.js b/server/db/photos.js
similarity index 84%
rename from server/db/index.js
rename to server/db/photos.js
index ec5f07f..72f6193 100644
--- a/server/db/index.js
+++ b/server/db/photos.js
@@ -20,12 +20,10 @@ const fs = require('fs'),
function init() {
const db = {
- sequelize: new Sequelize(config.get("db.host"), config.get("db.options")),
+ sequelize: new Sequelize(config.get("db.photos.host"), config.get("db.photos.options")),
Sequelize: Sequelize
};
- console.log("DB initialization beginning. DB access will block.");
-
return db.sequelize.authenticate().then(function () {
const Album = db.sequelize.define('album', {
id: {
@@ -74,19 +72,16 @@ function init() {
timestamps: false
});
-
- console.log("Connection established successfully with DB.");
return db.sequelize.sync({
force: false
}).then(function () {
- console.log("DB relationships successfully mapped. DB access unblocked.");
return db;
});
}).catch(function (error) {
- console.log("ERROR: Failed to authenticate with DB");
+ console.log("ERROR: Failed to authenticate with PHOTOS DB");
console.log("ERROR: " + JSON.stringify(config.get("db"), null, 2));
console.log(error);
- return reject(error);
+ throw error;
});
}
diff --git a/server/db/users.js b/server/db/users.js
new file mode 100644
index 0000000..e8baedb
--- /dev/null
+++ b/server/db/users.js
@@ -0,0 +1,60 @@
+/**
+ * * Copyright (c) 2016, Intel Corporation.
+ *
+ * This program is licensed under the terms and conditions of the
+ * Apache License, version 2.0. The full text of the Apache License is at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+"use strict";
+
+/**
+ * This class will instantiate the ORM, load in the models, call the method
+ * to create db connections, test the connection, then create the tables and
+ * relationships if not present
+ */
+const fs = require('fs'),
+ path = require('path'),
+ Sequelize = require('sequelize'),
+ config = require('config');
+
+function init() {
+ const db = {
+ sequelize: new Sequelize(config.get("db.users.host"), config.get("db.users.options")),
+ Sequelize: Sequelize
+ };
+
+ return db.sequelize.authenticate().then(function () {
+ const User = db.sequelize.define('users', {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ displayName: Sequelize.STRING,
+ uid: Sequelize.STRING,
+ isLDAP: Sequelize.BOOLEAN,
+ authToken: Sequelize.STRING,
+ authDate: Sequelize.DATE,
+ authenticated: Sequelize.BOOLEAN,
+ mail: Sequelize.STRING,
+ memberSince: Sequelize.DATE,
+ password: Sequelize.STRING, /* SHA hash of user supplied password for !isLDAP users */
+ passwordExpires: Sequelize.DATE
+ }, {
+ timestamps: false
+ });
+ return db.sequelize.sync({
+ force: false
+ }).then(function () {
+ return db;
+ });
+ }).catch(function (error) {
+ console.log("ERROR: Failed to authenticate with USER DB");
+ console.log("ERROR: " + JSON.stringify(config.get("db"), null, 2));
+ console.log(error);
+ throw error;
+ });
+}
+
+module.exports = init();
diff --git a/server/routes/albums.js b/server/routes/albums.js
index d99d8f1..c647a75 100644
--- a/server/routes/albums.js
+++ b/server/routes/albums.js
@@ -8,7 +8,7 @@ const express = require("express"),
let photoDB;
-require("../db").then(function(db) {
+require("../db/photos").then(function(db) {
photoDB = db;
});
@@ -36,7 +36,7 @@ router.get("/*", function(req, res/*, next*/) {
}
}
- return photoDB.sequelize.query("SELECT * FROM albums WHERE parentId=:parentId ORDER BY name", {
+ return photoDB.sequelize.query("SELECT * FROM albums WHERE parentId=:parentId ORDER BY LOWER(name)", {
replacements: {
parentId: parent.id
},
diff --git a/server/routes/days.js b/server/routes/days.js
index 959a4c8..47b94b2 100644
--- a/server/routes/days.js
+++ b/server/routes/days.js
@@ -8,7 +8,7 @@ const express = require("express"),
let photoDB;
-require("../db").then(function(db) {
+require("../db/photos").then(function(db) {
photoDB = db;
});
diff --git a/server/routes/index.js b/server/routes/index.js
index d335938..253ca00 100755
--- a/server/routes/index.js
+++ b/server/routes/index.js
@@ -30,7 +30,7 @@ const extensionMatch = new RegExp("^.*?(" + extensions.join("|") + ")$", "i");
* If so, 404 because the asset isn't there. otherwise assume it is a
* dynamic client side route and *then* return index.html.
*/
-router.get("/*", function(req, res/*, next*/) {
+router.get("/", function(req, res/*, next*/) {
const parts = url.parse(req.url),
basePath = req.app.get("basePath");
diff --git a/server/routes/photos.js b/server/routes/photos.js
index 3a727dd..5ac0f6e 100755
--- a/server/routes/photos.js
+++ b/server/routes/photos.js
@@ -8,7 +8,7 @@ const express = require("express"),
let photoDB;
-require("../db").then(function(db) {
+require("../db/photos").then(function(db) {
photoDB = db;
});
diff --git a/server/routes/users.js b/server/routes/users.js
new file mode 100755
index 0000000..5970c24
--- /dev/null
+++ b/server/routes/users.js
@@ -0,0 +1,91 @@
+"use strict";
+
+const express = require("express"),
+ config = require("config"),
+ LdapAuth = require("ldapauth-fork");
+
+const router = express.Router();
+
+let userDB;
+
+
+const ldap = new LdapAuth(config.get("ldap"));
+
+require("../db/users").then(function(db) {
+ userDB = db;
+});
+
+router.get("/", function(req, res/*, next*/) {
+ if (req.session.user) {
+ return res.status(200).send(req.session.user);
+ }
+ return res.status(200).send({});
+});
+
+function ldapPromise(username, password) {
+ return new Promise(function(resolve, reject) {
+ ldap.authenticate(username, password, function(error, user) {
+ if (error) {
+ return reject(error);
+ }
+ return resolve(user);
+ });
+ });
+}
+
+router.post("/login", function(req, res) {
+ let username = req.query.u || req.body.u || "",
+ password = req.query.p || req.body.p || "";
+
+ console.log("Login attempt");
+
+ if (!username || !password) {
+ return res.status(400).send("Missing username and/or password");
+ }
+
+ /* We use LDAP as the primary authenticator; if the user is not
+ * found there, we look them up in the site-specific user database */
+
+ return ldapPromise(username, password).then(function(user) {
+ return user;
+ }).catch(function() {
+ let query = "SELECT * FROM users WHERE username=:username";
+ return userDB.sequelize.query(query, {
+ replacements: {
+ username: username,
+ },
+ type: userDB.Sequelize.QueryTypes.SELECT
+ }).then(function(users) {
+ if (users.length != 1) {
+ return null;
+ }
+
+ return users[0];
+ });
+ }).then(function(user) {
+ if (!user) {
+ console.log(username + " not found: " + error);
+ req.session.user = {};
+ return res.status(401).send("Invalid login credentials");
+ }
+
+ console.log("Logging in as " + user.displayName);
+
+ req.session.user = {
+ name: user.displayName,
+ mail: user.mail,
+ username: user.uid
+ };
+
+ return res.status(200).send(req.session.user);
+ });
+});
+
+router.get("/logout", function(req, res) {
+ if (req.session && req.session.user) {
+ req.session.user = {};
+ }
+ res.status(200).send(req.session.user);
+});
+
+module.exports = router;