Added LDAP login and forced login

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2018-09-16 20:58:37 -07:00
parent 4228939b55
commit c2ae12df5c
12 changed files with 390 additions and 40 deletions

View File

@ -1,14 +1,32 @@
{
"db": {
"host": "sqlite:photos.db",
"options": {
"logging" : false,
"timezone": "+00:00"
"photos": {
"host": "sqlite:photos.db",
"options": {
"logging" : false,
"timezone": "+00:00"
}
},
"users": {
"host": "sqlite:users.db",
"options": {
"logging" : false,
"timezone": "+00:00"
}
}
},
"ldap": {
"searchFilter": "(uid={{username}})",
"cache": true
},
"server": {
"port": 8123
},
"picturesPath": "./pictures",
"basePath": "/photos"
"basePath": "/photos",
"sessions": {
"db": "sessions.db",
"store-secret": "234j23jffj23f!41$@#!1113j3"
}
}

View File

@ -29,7 +29,9 @@
"paper-button": "PolymerElements/paper-button#^2",
"paper-spinner": "PolymerElements/paper-spinner#^2",
"paper-toast": "PolymerElements/paper-toast#^2",
"iron-iconset": "PolymerElements/iron-iconset#^2"
"iron-iconset": "PolymerElements/iron-iconset#^2",
"paper-input": "PolymerElements/paper-input#^2",
"paper-dialog": "PolymerElements/paper-dialog#^2"
},
"resolutions": {
"polymer": "2",

View File

@ -16,6 +16,8 @@
<link rel="import" href="../../bower_components/iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="../../bower_components/paper-button/paper-button.html">
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
<link rel="import" href="../../bower_components/paper-tabs/paper-tab.html">
@ -51,6 +53,13 @@
:host {
}
#login {
margin: 1em;
padding: 2em;
border: 1px solid #444;
box-sizing: border-box;
}
#header {
padding: 0.5em;
background: #ddd;
@ -60,7 +69,7 @@
}
#header iron-pages {
padding-left: 0.5em;
padding: 0 0.5em;
}
#header [mode='memories'] b:hover {
@ -250,6 +259,14 @@
#memories [is-today] paper-icon-button {
}
#requestAccess div > div {
padding: 0.5em;
}
#requestAccess .title {
background-color: #ddd;
}
</style>
<app-location route="{{route}}"></app-location>
@ -261,7 +278,7 @@
<paper-tab tab="memories"><paper-icon-button icon="today"></paper-icon-button></paper-tab>
<paper-tab tab="albums"><paper-icon-button icon="folder"></paper-icon-button></paper-tab>
</paper-tabs>
<iron-pages id="pages" attr-for-selected="id" selected="[[mode]]">
<iron-pages id="pages" attr-for-selected="id" selected="[[mode]]" fallback-selection="memories">
<div id="time"><div>... time slider ...</div></div>
<div id="memories" class="flex layout vertical center">
<div class="memory-buttons layout self-stretch horizontal around-justified">
@ -299,8 +316,9 @@
<app-header reveals slot="header">
<div id="header" class="layout horizontal center">
<paper-icon-button icon="search" on-tap="drawerToggle"></paper-icon-button>
<iron-pages attr-for-selected="mode" selected="[[mode]]">
<div mode="albums" id="breadcrumb" class="horizontal layout center">
<iron-pages class="flex" attr-for-selected="mode" selected="[[mode]]">
<div mode="login"><div>You are not logged in.</div></div>
<div mode="albums" id="breadcrumb" class="horizontal layout center">
<template is="dom-repeat" items="[[breadcrumb(path)]]">
<div tabindex="0" on-tap="loadPath">[[item.name]] /</div>
</template>
@ -308,14 +326,42 @@
<div mode="time">time</div>
<div mode="memories">Photos taken on <b on-tap="drawerToggle">[[memoryDate]]</b></div>
</iron-pages>
<div>
<div hidden$="[[user]]">
<!--paper-button>login</paper-button-->
</div>
<div hidden$="[[!user]]">
<paper-button on-tap="logout">logout</paper-button>
</div>
</div>
</div>
</app-header>
<div id="thumbnails" class="layout horizontal wrap"></div>
<div>
<div hidden$="[[!user]]">
<div id="thumbnails" class="layout horizontal wrap"></div>
<div id="bottom" class="layout vertical center">
<paper-spinner hidden$="[[!loading]]" active$="[[loading]]" class="thin"></paper-spinner>
<div hidden$="[[loading]]">~ the end ~</div>
</div>
</div>
<div id="login" hidden$="[[user]]" class="layout horizontal center">
<div class="flex layout vertical">
<div id="instructions">
<p><b>ketrenos.com</b> is a personal website for my family and friends.</p>
<p>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.</p>
</p>If you are a friend or family member (immediate, or extended)
<a on-tap="requestAccess" href="#">request access</a>,
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 :)</p>
</div>
<paper-input tabindex autofocus id="username" label="User ID" value="{{username}}" on-keypress="enterCheck"></paper-input>
<paper-input tabindex id="password" label="Password" type="password" value="{{password}}" on-keypress="enterCheck"></paper-input>
<paper-button tabindex disabled$="[[disableLogin(username,password)]]" on-tap="login">login</paper-button>
</div>
</div>
</div>
</app-header-layout>
<div id="bottom" class="layout vertical center">
<paper-spinner hidden$="[[!loading]]" active$="[[loading]]" class="thin"></paper-spinner>
<div hidden$="[[loading]]">~ the end ~</div>
</div>
</app-drawer-layout>
<div id="pager">pager</div>
<div id="yearSlider">
@ -323,6 +369,16 @@
<div>item</div>
</template>
</div>
<paper-dialog id="requestAccess" modal>
<div class="layout vertical">
<div class="title">Hello!</div>
<div>
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.
</div>
<paper-button dialog-dismiss>close</paper-button>
</div>
</paper-dialog>
<paper-toast id="toast"></paper-toast>
<photo-lightbox tabindex="0" id="lightbox" on-close="lightBoxClose" on-next="lightBoxNext" on-previous="lightBoxPrevious"></photo-lightbox>
</template>
@ -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();

View File

@ -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",

View File

@ -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;

View File

@ -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;
});
}

60
server/db/users.js Normal file
View File

@ -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();

View File

@ -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
},

View File

@ -8,7 +8,7 @@ const express = require("express"),
let photoDB;
require("../db").then(function(db) {
require("../db/photos").then(function(db) {
photoDB = db;
});

View File

@ -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");

View File

@ -8,7 +8,7 @@ const express = require("express"),
let photoDB;
require("../db").then(function(db) {
require("../db/photos").then(function(db) {
photoDB = db;
});

91
server/routes/users.js Executable file
View File

@ -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;