1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
James Ketrenos
d21946cb9e Added eslintrc
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-04-07 14:25:37 -07:00
James Ketrenos
c7435c4bb1 eslinted everything and moved db around
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-04-07 14:25:20 -07:00
17 changed files with 1698 additions and 500 deletions

26
.eslintrc.json Normal file
View File

@ -0,0 +1,26 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}

View File

@ -26,20 +26,43 @@ const App = () => {
const [ csrfToken, setCsrfToken ] = useState(undefined); const [ csrfToken, setCsrfToken ] = useState(undefined);
useEffect(() => { useEffect(() => {
window.fetch(`${base}/api/v1/users/csrf`, { const effect = async () => {
method: 'GET', const res = await window.fetch(`${base}/api/v1/users/csrf`, {
cache: 'no-cache', method: 'GET',
credentials: 'same-origin', cache: 'no-cache',
headers: { credentials: 'same-origin',
'Content-Type': 'application/json' headers: {
}, 'Content-Type': 'application/json'
}).then((res) => { },
return res.json(); });
}).then((data) => { const data = await res.json();
setCsrfToken(data.csrfToken); setCsrfToken(data.csrfToken);
}); }
effect();
}, []); }, []);
useEffect(() => {
if (csrfToken) {
return;
}
const effect = async () => {
const res = await window.fetch(`${base}/api/v1/users`, {
method: 'GET',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken
},
});
const data = await res.json();
setUser(data);
};
effect();
}, [csrfToken, setUser]);
return ( return (
<div className="App"> <div className="App">
<GlobalContext.Provider value={{user, setUser, csrfToken }}> <GlobalContext.Provider value={{user, setUser, csrfToken }}>

16
server/.eslintrc.json Normal file
View File

@ -0,0 +1,16 @@
{
"env": {
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"indent": [
"error",
2
]
}
}

View File

@ -1,73 +1,41 @@
"use strict"; 'use strict';
process.env.TZ = "Etc/GMT"; process.env.TZ = 'Etc/GMT';
console.log("Loading Goodtimes"); console.log('Loading Goodtimes');
const express = require("express"), const express = require('express'),
bodyParser = require("body-parser"), bodyParser = require('body-parser'),
config = require("config"), config = require('config'),
session = require('express-session'), session = require('express-session'),
basePath = require("./basepath"), basePath = require('./basepath'),
cookieParser = require("cookie-parser"), cookieParser = require('cookie-parser'),
app = express(), app = express(),
fs = require('fs'), csrf = require('csurf'),
csrf = require('csurf'); http = require('http'),
{ goodTimesDB } = require('./db.js');
const server = require("http").createServer(app); require('./console-line.js'); /* Monkey-patch console.log with line numbers */
/* 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(session({ app.use(session({
secret: 'm@g1x!', secret: 'm@g1x!',
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
cookie: { secure: true } cookie: { secure: true }
})); }));
app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser()) app.use(cookieParser());
app.use(csrf({ app.use(csrf({
cookie: true cookie: true
})); }));
app.use(bodyParser.json());
//const ws = require('express-ws')(app, server); //const ws = require('express-ws')(app, server);
require("./console-line.js"); /* Monkey-patch console.log with line numbers */ app.use(function (err, req, res) {
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/",
serverConfig = config.get("server");
console.log("Hosting server from: " + basePath);
let userDB, groupDB;
app.use(bodyParser.json());
/* App is behind an nginx proxy which we trust, so use the remote address
* set in the headers */
app.set("trust proxy", true);
app.set("basePath", basePath);
app.use(basePath, require("./routes/basepath.js"));
/* Handle static files first so excessive logging doesn't occur */
app.use(basePath, express.static(frontendPath, { index: false }));
const index = require("./routes/index");
if (config.has("admin")) {
const admin = config.get("admin");
app.set("admin", admin);
}
/* 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) {
console.error(err.message); console.error(err.message);
res.status(err.status || 500).json({ res.status(err.status || 500).json({
message: err.message, message: err.message,
@ -75,55 +43,47 @@ app.use(function(err, req, res, next) {
}); });
}); });
app.use(`${basePath}api/v1/groups`, require("./routes/groups")); /* Initialize the data base, configure routes, and open server */
app.use(`${basePath}api/v1/users`, require("./routes/users")); goodTimesDB.init().then((db) => {
console.log('DB connected. Configuring routes.');
app.locals.db = db;
app.set('basePath', basePath);
app.use(`${basePath}api/v1/groups`, require('./routes/groups'));
app.use(`${basePath}api/v1/users`, require('./routes/users'));
}).then(() => {
const server = http.createServer(app),
serverConfig = config.get('server');
/* Declare the "catch all" index route last; the final route is a 404 dynamic router */ console.log('Hosting server from: ' + basePath);
app.use(basePath, index); app.set('port', serverConfig.port);
/** process.on('SIGINT', () => {
* Create HTTP server and listen for new connections console.log('Gracefully shutting down from SIGINT (Ctrl-C) in 2 seconds');
*/ setTimeout(() => process.exit(-1), 2000);
app.set("port", serverConfig.port); server.close(() => process.exit(1));
process.on('SIGINT', () => {
console.log("Gracefully shutting down from SIGINT (Ctrl-C) in 2 seconds");
setTimeout(() => process.exit(-1), 2000);
server.close(() => process.exit(1));
});
require("./db/groups").then(function(db) {
groupDB = db;
}).then(function() {
return require("./db/users").then(function(db) {
userDB = db;
}); });
}).then(function() {
console.log("DB connected. Opening server.");
server.listen(serverConfig.port, () => {
console.log(`http server listening on ${serverConfig.port}`);
});
}).catch(function(error) {
console.error(error);
process.exit(-1);
});
server.on("error", function(error) { server.on('error', function (error) {
if (error.syscall !== "listen") { if (error.syscall !== 'listen') {
throw error; throw error;
} }
// handle specific listen errors with friendly messages // handle specific listen errors with friendly messages
switch (error.code) { switch (error.code) {
case "EACCES": case 'EACCES':
console.error(serverConfig.port + " requires elevated privileges"); console.error(serverConfig.port + ' requires elevated privileges');
process.exit(1); process.exit(1);
break; break;
case "EADDRINUSE": case 'EADDRINUSE':
console.error(serverConfig.port + " is already in use"); console.error(serverConfig.port + ' is already in use');
process.exit(1); process.exit(1);
break; break;
default: default:
throw error; throw error;
} }
}); });
server.listen(serverConfig.port, () => {
console.log(`http server listening on ${serverConfig.port}`);
});
});

View File

@ -1,7 +1,7 @@
let basePath = process.env.REACT_APP_basePath; let basePath = process.env.REACT_APP_basePath;
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/"; basePath = '/' + basePath.replace(/^\/+/, '').replace(/\/+$/, '') + '/';
if (basePath == "//") { if (basePath == '//') {
basePath = "/"; basePath = '/';
} }
console.log(`Using basepath ${basePath}`); console.log(`Using basepath ${basePath}`);

View File

@ -1,15 +1,9 @@
{ {
"db": { "db": {
"groups": { "goodtimes": {
"dialect": "sqlite", "dialect": "sqlite",
"storage": "../db/groups.db", "storage": "../db/goodtimes.db",
"logging" : false, "logging": false,
"timezone": "+00:00"
},
"users": {
"dialect": "sqlite",
"storage": "../db/users.db",
"logging" : false,
"timezone": "+00:00" "timezone": "+00:00"
} }
}, },

View File

@ -1,8 +1,8 @@
/* monkey-patch console.log to prefix with file/line-number */ /* monkey-patch console.log to prefix with file/line-number */
if (process.env.LOG_LINE) { if (process.env.LOG_LINE) {
let cwd = process.cwd(), let cwd = process.cwd(),
cwdRe = new RegExp("^[^/]*" + cwd.replace("/", "\\/") + "\/([^:]*:[0-9]*).*$"); cwdRe = new RegExp('^[^/]*' + cwd.replace('/', '\\/') + '\/([^:]*:[0-9]*).*$');
[ "log", "warn", "error" ].forEach(function(method) { [ 'log', 'warn', 'error' ].forEach(function(method) {
console[method] = (function () { console[method] = (function () {
let orig = console[method]; let orig = console[method];
return function () { return function () {
@ -15,8 +15,8 @@ if (process.env.LOG_LINE) {
} }
let err = getErrorObject(), let err = getErrorObject(),
caller_line = err.stack.split("\n")[3], caller_line = err.stack.split('\n')[3],
args = [caller_line.replace(cwdRe, "$1 -")]; args = [caller_line.replace(cwdRe, '$1 -')];
/* arguments.unshift() doesn't exist... */ /* arguments.unshift() doesn't exist... */
for (var i = 0; i < arguments.length; i++) { for (var i = 0; i < arguments.length; i++) {

50
server/db/users.js → server/db.js Executable file → Normal file
View File

@ -1,15 +1,30 @@
"use strict"; 'use strict';
const Sequelize = require('sequelize'), const Sequelize = require('sequelize'),
config = require('config'); config = require('config');
function init() { function init() {
const db = { const db = {
sequelize: new Sequelize(config.get("db.users")), sequelize: new Sequelize(config.get('db.goodtimes')),
Sequelize: Sequelize Sequelize: Sequelize
}; };
return db.sequelize.authenticate().then(function () { return db.sequelize.authenticate().then(function () {
const Group = db.sequelize.define('group', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: Sequelize.STRING,
ownerId: Sequelize.INTEGER
}, {
timestamps: false,
classMethods: {
associate: function() {}
}
});
const User = db.sequelize.define('users', { const User = db.sequelize.define('users', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@ -32,6 +47,7 @@ function init() {
timestamps: false timestamps: false
}); });
// eslint-disable-next-line no-unused-vars
const Authentication = db.sequelize.define('authentication', { const Authentication = db.sequelize.define('authentication', {
key: { key: {
type: Sequelize.STRING, type: Sequelize.STRING,
@ -41,7 +57,7 @@ function init() {
issued: Sequelize.DATE, issued: Sequelize.DATE,
type: { type: {
type: Sequelize.ENUM, type: Sequelize.ENUM,
values: [ 'account-setup', 'password-reset' ] values: ['account-setup', 'password-reset']
}, },
userId: { userId: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@ -54,6 +70,28 @@ function init() {
}, { }, {
timestamps: false timestamps: false
}); });
// eslint-disable-next-line no-unused-vars
const GroupUsers = db.sequelize.define('groupuser', {
groupId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: Group,
key: 'id'
}
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: User,
key: 'id'
}
}
}, {
timestamps: false
});
return db.sequelize.sync({ return db.sequelize.sync({
force: false force: false
@ -61,11 +99,11 @@ function init() {
return db; return db;
}); });
}).catch(function (error) { }).catch(function (error) {
console.log("ERROR: Failed to authenticate with USER DB"); console.log('ERROR: Failed to authenticate with GROUP DB');
console.log("ERROR: " + JSON.stringify(config.get("db"), null, 2)); console.log('ERROR: ' + JSON.stringify(config.get('db'), null, 2));
console.log(error); console.log(error);
throw error; throw error;
}); });
} }
module.exports = init(); module.exports = { goodTimesDB: { init } };

View File

@ -1,58 +0,0 @@
"use strict";
const Sequelize = require('sequelize'),
config = require('config');
function init() {
const db = {
sequelize: new Sequelize(config.get("db.groups")),
Sequelize: Sequelize
};
return db.sequelize.authenticate().then(function () {
const Group = db.sequelize.define('group', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: Sequelize.STRING,
ownerId: Sequelize.INTEGER
}, {
timestamps: false,
classMethods: {
associate: function() {}
}
});
const GroupUsers = db.sequelize.define('groupuser', {
groupId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: Group,
key: 'id'
}
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
}
}, {
timestamps: false
});
return db.sequelize.sync({
force: false
}).then(function () {
return db;
});
}).catch(function (error) {
console.log("ERROR: Failed to authenticate with GROUP DB");
console.log("ERROR: " + JSON.stringify(config.get("db"), null, 2));
console.log(error);
throw error;
});
}
module.exports = init();

View File

@ -1,61 +0,0 @@
"use strict";
const createTransport = require('nodemailer').createTransport,
{ timestamp } = require("./timestamp");
const transporter = createTransport({
host: 'email.ketrenos.com',
pool: true,
port: 25
});
function sendMail(to, subject, message, cc) {
let envelope = {
subject: subject,
from: 'Ketr.Ketran <james_ketran@ketrenos.com>',
to: to || '',
cc: cc || ''
};
/* If there isn't a To: but there is a Cc:, promote Cc: to To: */
if (!envelope.to && envelope.cc) {
envelope.to = envelope.cc;
delete envelope.cc;
}
envelope.text = message
envelope.html = message.replace(/\n/g, "<br>\n");
return new Promise(function (resolve, reject) {
let attempts = 10;
function attemptSend(envelope) {
/* Rate limit to ten per second */
transporter.sendMail(envelope, function (error, info) {
if (error) {
if (attempts) {
attempts--;
console.warn(timestamp() + " Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error);
setTimeout(send.bind(undefined, envelope), 100);
} else {
console.error(timestamp() + " Error sending email: ", error)
return reject(error);
}
}
console.log(timestamp() + " Mail sent to: " + envelope.to);
return resolve(true);
});
}
attemptSend(envelope);
}).then(function(success) {
if (!success) {
console.error(timestamp() + " Mail not sent to: " + envelope.to);
}
});
}
module.exports = {
sendMail: sendMail
};

1401
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,5 +38,8 @@
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@git.ketrenos.com:jketreno/goodtimes" "url": "git@git.ketrenos.com:jketreno/goodtimes"
},
"devDependencies": {
"eslint": "^8.12.0"
} }
} }

View File

@ -1,33 +0,0 @@
"use strict";
const express = require("express"),
fs = require("fs"),
url = require("url");
const router = express.Router();
/* This router only handles HTML files and is used
* to replace BASEPATH */
router.get("/*", (req, res, next) => {
const parts = url.parse(req.url),
basePath = req.app.get("basePath");
if (!/^\/[^/]+\.html$/.exec(parts.pathname)) {
return next();
}
console.log("Attempting to parse 'frontend" + parts.pathname + "'");
/* Replace <script>'<base href="/BASEPATH/">';</script> in index.html with
* the basePath */
fs.readFile("frontend" + parts.pathname, "utf8", function(error, content) {
if (error) {
return next();
}
res.send(content.replace(
/<script>'<base href="BASEPATH">';<\/script>/,
"<base href='" + basePath + "'>"));
});
});
module.exports = router;

View File

@ -1,30 +1,10 @@
"use strict"; const express = require('express'),
const express = require("express"),
router = express.Router(), router = express.Router(),
crypto = require("crypto"), crypto = require('crypto');
{ readFile, writeFile } = require("fs").promises,
fs = require("fs"),
accessSync = fs.accessSync,
randomWords = require("random-words"),
equal = require("fast-deep-equal");
const debug = {
audio: false,
get: true,
set: true,
update: false
};
let groupDB;
require("../db/groups").then(function(db) {
groupDB = db;
});
/* Simple NO-OP to set session cookie so user-id can use it as the /* Simple NO-OP to set session cookie so user-id can use it as the
* index */ * index */
router.get("/", (req, res/*, next*/) => { router.get('/', (req, res/*, next*/) => {
let userId; let userId;
if (!req.cookies.user) { if (!req.cookies.user) {
userId = crypto.randomBytes(16).toString('hex'); userId = crypto.randomBytes(16).toString('hex');
@ -38,19 +18,19 @@ router.get("/", (req, res/*, next*/) => {
return res.status(200).send({ user: userId }); return res.status(200).send({ user: userId });
}); });
router.post("/", async (req, res/*, next*/) => { router.get('/', async (req, res/*, next*/) => {
console.log(`POST /groups/`, req.session.userId); console.log('GET /groups/', req.session.userId);
return res.status(200).send( return res.status(200).send(
[ { [ {
id: 1, id: 1,
ownerId: 1, ownerId: 1,
name: "Beer Tuesday", name: 'Beer Tuesday',
nextEvent: Date.now() + 86400 * 14 * 1000 /* 2 weeks from now */ nextEvent: Date.now() + 86400 * 14 * 1000 /* 2 weeks from now */
} ] } ]
); );
}); });
router.post("/:id", async (req, res/*, next*/) => { router.post('/:id', async (req, res) => {
const { id } = req.params; const { id } = req.params;
let userId; let userId;
@ -61,6 +41,10 @@ router.post("/:id", async (req, res/*, next*/) => {
userId = req.cookies.user; userId = req.cookies.user;
} }
const group = {
id: 1
};
if (id) { if (id) {
console.log(`[${userId.substring(0,8)}]: Attempting load of ${id}`); console.log(`[${userId.substring(0,8)}]: Attempting load of ${id}`);
} else { } else {

View File

@ -1,65 +0,0 @@
"use strict";
const express = require("express"),
fs = require("fs"),
url = require("url"),
config = require("config"),
basePath = require("../basepath");
const router = express.Router();
/* List of filename extensions we know are "potential" file extensions for
* assets we don"t want to return "index.html" for */
const extensions = [
"html", "js", "css", "eot", "gif", "ico", "jpeg", "jpg", "mp4",
"md", "ttf", "txt", "woff", "woff2", "yml", "svg"
];
/* Build the extension match RegExp from the list of extensions */
const extensionMatch = new RegExp("^.*?(" + extensions.join("|") + ")$", "i");
/* To handle dynamic routes, we return index.html to every request that
* gets this far -- so this needs to be the last route.
*
* However, that introduces site development problems when assets are
* referenced which don't yet exist (due to bugs, or sequence of adds) --
* the server would return HTML content instead of the 404.
*
* So, check to see if the requested path is for an asset with a recognized
* file extension.
*
* 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) {
const parts = url.parse(req.url);
/* If req.user isn't set yet (authentication hasn't happened) then
* only allow / to be loaded--everything else chains to the next
* handler */
if (!req.user &&
req.url !== "/") {
return next();
}
if (req.url == "/" || req.url.indexOf("/games") == 0 || !extensionMatch.exec(parts.pathname)) {
console.log("Returning index for " + req.url);
/* Replace <script>'<base href="BASEPATH">';</script> in index.html with
* the basePath */
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/",
index = fs.readFileSync(frontendPath + "index.html", "utf8");
res.send(index.replace(
/<script>'<base href="BASEPATH">';<\/script>/,
"<base href='" + basePath + "'>"));
return;
}
console.log("Page not found: " + req.url);
return res.status(404).json({
message: "Page not found",
status: 404
});
});
module.exports = router;

View File

@ -1,31 +1,26 @@
"use strict"; 'use strict';
const express = require("express"), const express = require('express'),
config = require("config"), { sendVerifyMail, sendPasswordChangedMail, sendVerifiedMail } =
{ sendVerifyMail, sendPasswordChangedMail, sendVerifiedMail } = require("../lib/mail"), require('../lib/mail'),
crypto = require("crypto"); crypto = require('crypto');
const router = express.Router(); const router = express.Router();
const autoAuthenticate = 1; const autoAuthenticate = 1;
let userDB; router.get('/', function(req, res/*, next*/) {
console.log('GET /users/');
require("../db/users.js").then(function(db) {
userDB = db;
});
router.get("/", function(req, res/*, next*/) {
console.log("/users/");
return getSessionUser(req).then((user) => { return getSessionUser(req).then((user) => {
return res.status(200).send(user); return res.status(200).send(user);
}).catch((error) => { }).catch((error) => {
console.log("User not logged in: " + error); console.log('User not logged in: ' + error);
return res.status(200).send({}); return res.status(200).send({});
}); });
}); });
router.put("/password", function(req, res) { router.put('/password', function(req, res) {
console.log("/users/password"); console.log('/users/password');
const db = req.app.locals.db;
const changes = { const changes = {
currentPassword: req.query.c || req.body.c, currentPassword: req.query.c || req.body.c,
@ -33,21 +28,21 @@ router.put("/password", function(req, res) {
}; };
if (!changes.currentPassword || !changes.newPassword) { if (!changes.currentPassword || !changes.newPassword) {
return res.status(400).send("Missing current password and/or new password."); return res.status(400).send('Missing current password and/or new password.');
} }
if (changes.currentPassword == changes.newPassword) { if (changes.currentPassword == changes.newPassword) {
return res.status(400).send("Attempt to set new password to current password."); return res.status(400).send('Attempt to set new password to current password.');
} }
return getSessionUser(req).then(function(user) { return getSessionUser(req).then(function(user) {
return userDB.sequelize.query("SELECT id FROM users " + return db.sequelize.query('SELECT id FROM users ' +
"WHERE uid=:username AND password=:password", { 'WHERE uid=:username AND password=:password', {
replacements: { replacements: {
username: user.username, username: user.username,
password: crypto.createHash('sha256').update(changes.currentPassword).digest('base64') password: crypto.createHash('sha256').update(changes.currentPassword).digest('base64')
}, },
type: userDB.Sequelize.QueryTypes.SELECT, type: db.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}).then(function(users) { }).then(function(users) {
if (users.length != 1) { if (users.length != 1) {
@ -57,34 +52,35 @@ router.put("/password", function(req, res) {
}); });
}).then(function(user) { }).then(function(user) {
if (!user) { if (!user) {
console.log("Invalid password"); console.log('Invalid password');
/* Invalid password */ /* Invalid password */
res.status(401).send("Invalid password"); res.status(401).send('Invalid password');
return null; return null;
} }
return userDB.sequelize.query("UPDATE users SET password=:password WHERE uid=:username", { return db.sequelize.query('UPDATE users SET password=:password WHERE uid=:username', {
replacements: { replacements: {
username: user.username, username: user.username,
password: crypto.createHash('sha256').update(changes.newPassword).digest('base64') password: crypto.createHash('sha256').update(changes.newPassword).digest('base64')
} }
}).then(function() { }).then(function() {
console.log("Password changed for user " + user.username + " to '" + changes.newPassword + "'."); console.log('Password changed for user ' + user.username + ' to \'' + changes.newPassword + '\'.');
res.status(200).send(user); res.status(200).send(user);
user.id = req.session.userId; user.id = req.session.userId;
return sendPasswordChangedMail(userDB, req, user); return sendPasswordChangedMail(db, req, user);
}); });
}); });
}); });
router.get("/csrf", (req, res) => { router.get('/csrf', (req, res) => {
console.log("/users/csrf"); console.log('/users/csrf');
res.json({ csrfToken: req.csrfToken() }); res.json({ csrfToken: req.csrfToken() });
}); });
router.post("/signup", function(req, res) { router.post('/signup', function(req, res) {
console.log("/users/signup"); console.log('/users/signup');
const db = req.app.locals.db;
const user = { const user = {
uid: req.body.email, uid: req.body.email,
@ -100,7 +96,7 @@ router.post("/signup", function(req, res) {
|| !user.familyName || !user.familyName
|| !user.firstName) { || !user.firstName) {
return res.status(400).send({ return res.status(400).send({
message: `Missing email address, password, and/or name.` message: 'Missing email address, password, and/or name.'
}); });
} }
@ -109,18 +105,18 @@ router.post("/signup", function(req, res) {
user.md5 = crypto.createHash('md5') user.md5 = crypto.createHash('md5')
.update(user.email).digest('base64'); .update(user.email).digest('base64');
return userDB.sequelize.query("SELECT * FROM users WHERE uid=:uid", { return db.sequelize.query('SELECT * FROM users WHERE uid=:uid', {
replacements: user, replacements: user,
type: userDB.Sequelize.QueryTypes.SELECT, type: db.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}).then(async function(results) { }).then(async function(results) {
if (results.length != 0 && results[0].mailVerified) { if (results.length != 0 && results[0].mailVerified) {
return res.status(400).send({ return res.status(400).send({
message: `Email address already used.` message: '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,}))$/; 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(user.email)) { if (!re.exec(user.email)) {
const error = `Invalid email address: ${user.email}.`; const error = `Invalid email address: ${user.email}.`;
console.log(error); console.log(error);
@ -131,14 +127,14 @@ router.post("/signup", function(req, res) {
try { try {
if (results.length != 0) { if (results.length != 0) {
await userDB.sequelize.query("UPDATE users " + await db.sequelize.query('UPDATE users ' +
"SET mailVerified=0"); 'SET mailVerified=0');
req.session.userId = results[0].id; req.session.userId = results[0].id;
} else { } else {
let [, metadata] = await userDB.sequelize.query("INSERT INTO users " + let [, metadata] = await db.sequelize.query('INSERT INTO users ' +
"(uid,firstName,familyName,password,email,memberSince," + '(uid,firstName,familyName,password,email,memberSince,' +
"authenticated,md5) " + 'authenticated,md5) ' +
`VALUES(:uid,:firstName,:familyName,:password,` + 'VALUES(:uid,:firstName,:familyName,:password,' +
`:email,CURRENT_TIMESTAMP,${autoAuthenticate},:md5)`, { `:email,CURRENT_TIMESTAMP,${autoAuthenticate},:md5)`, {
replacements: user replacements: user
}); });
@ -147,7 +143,7 @@ router.post("/signup", function(req, res) {
return getSessionUser(req).then(function(user) { return getSessionUser(req).then(function(user) {
res.status(200).send(user); res.status(200).send(user);
user.id = req.session.userId; user.id = req.session.userId;
return sendVerifyMail(userDB, req, user); return sendVerifyMail(db, req, user);
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -156,34 +152,35 @@ router.post("/signup", function(req, res) {
}); });
const getSessionUser = function(req) { const getSessionUser = function(req) {
const db = req.app.locals.db;
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
if (!req.session || !req.session.userId) { if (!req.session || !req.session.userId) {
throw "Unauthorized. You must be logged in."; throw 'Unauthorized. You must be logged in.';
} }
let query = "SELECT " + let query = 'SELECT ' +
"uid AS username,firstName,familyName,mailVerified,authenticated,memberSince,email,md5 " + 'uid AS username,firstName,familyName,mailVerified,authenticated,memberSince,email,md5 ' +
"FROM users WHERE id=:id"; 'FROM users WHERE id=:id';
return userDB.sequelize.query(query, { return db.sequelize.query(query, {
replacements: { replacements: {
id: req.session.userId id: req.session.userId
}, },
type: userDB.Sequelize.QueryTypes.SELECT, type: db.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}).then(function(results) { }).then(function(results) {
if (results.length != 1) { if (results.length != 1) {
throw "Invalid account."; throw 'Invalid account.';
} }
let user = results[0]; let user = results[0];
if (!user.mailVerified) { if (!user.mailVerified) {
user.restriction = user.restriction || "Email address not verified."; user.restriction = user.restriction || 'Email address not verified.';
return user; return user;
} }
if (!user.authenticated) { if (!user.authenticated) {
user.restriction = user.restriction || "Accout not authorized."; user.restriction = user.restriction || 'Accout not authorized.';
return user; return user;
} }
@ -192,7 +189,7 @@ const getSessionUser = function(req) {
}).then(function(user) { }).then(function(user) {
/* Strip out any fields that shouldn't be there. The allowed fields are: */ /* Strip out any fields that shouldn't be there. The allowed fields are: */
let allowed = [ let allowed = [
"maintainer", "username", "firstName", "familyName", "mailVerified", "authenticated", "name", "email", "restriction", "md5" 'maintainer', 'username', 'firstName', 'familyName', 'mailVerified', 'authenticated', 'name', 'email', 'restriction', 'md5'
]; ];
for (let field in user) { for (let field in user) {
if (allowed.indexOf(field) == -1) { if (allowed.indexOf(field) == -1) {
@ -201,114 +198,114 @@ const getSessionUser = function(req) {
} }
return user; return user;
}); });
} };
router.post("/verify-email", async (req, res) => { router.post('/verify-email', async (req, res) => {
console.log("/users/verify-email"); console.log('/users/verify-email');
const key = req.body.token; const key = req.body.token;
const db = req.app.locals.db;
let results = await userDB.sequelize.query( let results = await db.sequelize.query(
"SELECT * FROM authentications WHERE key=:key", { 'SELECT * FROM authentications WHERE key=:key', {
replacements: { key }, replacements: { key },
type: userDB.sequelize.QueryTypes.SELECT type: db.sequelize.QueryTypes.SELECT
}); }
);
let token; let token;
if (results.length == 0) { if (results.length == 0) {
console.log("Invalid key. Ignoring."); console.log('Invalid key. Ignoring.');
return res.status(400).send({ return res.status(400).send({
message: `Invalid authentication token.` message: 'Invalid authentication token.'
}); });
} }
token = results[0]; token = results[0];
console.log(token); console.log(token);
console.log("Matched token: " + JSON.stringify(token, null, 2)); console.log('Matched token: ' + JSON.stringify(token, null, 2));
switch (token.type) { switch (token.type) {
case "account-setup": case 'account-setup':
return userDB.sequelize.query( return db.sequelize.query(
"UPDATE users SET mailVerified=1 WHERE id=:userId", { 'UPDATE users SET mailVerified=1 WHERE id=:userId', {
replacements: token replacements: token
}) }
.then(function () { ).then(function () {
return userDB.sequelize.query( return db.sequelize.query(
"DELETE FROM authentications WHERE key=:key", { 'DELETE FROM authentications WHERE key=:key', {
replacements: { key } replacements: { key }
}); }
}) );
.then(function () { }).then(function () {
return userDB.sequelize.query( return db.sequelize.query(
"SELECT * FROM users WHERE id=:userId", { 'SELECT * FROM users WHERE id=:userId', {
replacements: token, replacements: token,
type: userDB.sequelize.QueryTypes.SELECT type: db.sequelize.QueryTypes.SELECT
}); }
}) );
.then(function (results) { }).then(function (results) {
if (results.length == 0) { if (results.length == 0) {
return res.status(500).send({ return res.status(500).send({
message: `Internal authentication error.` message: 'Internal authentication error.'
}); });
} }
return results[0]; return results[0];
}) }).then((user) => {
.then((user) => { sendVerifiedMail(db, req, user);
sendVerifiedMail(userDB, req, user);
req.session.userId = user.id; req.session.userId = user.id;
}).then(function (user) { }).then(() => {
return getSessionUser(req).then(function (user) { return getSessionUser(req).then((user) => {
return res.status(200).send(user); return res.status(200).send(user);
}); });
}); });
} }
}); });
router.post("/signin", function(req, res) { router.post('/signin', function(req, res) {
console.log("/users/signin"); console.log('/users/signin');
const db = req.app.locals.db;
let { email, password } = req.body; let { email, password } = req.body;
if (!email || !password) { if (!email || !password) {
return res.status(400).send({ return res.status(400).send({
message: `Missing email and/or password` message: 'Missing email and/or password'
}); });
} }
console.log("Looking up user in DB."); console.log('Looking up user in DB.');
let query = "SELECT " + let query = 'SELECT ' +
"id,mailVerified,authenticated," + 'id,mailVerified,authenticated,' +
"uid AS username," + 'uid AS username,' +
"familyName,firstName,email " + 'familyName,firstName,email ' +
"FROM users WHERE uid=:username AND password=:password"; 'FROM users WHERE uid=:username AND password=:password';
return userDB.sequelize.query(query, { return db.sequelize.query(query, {
replacements: { replacements: {
username: email, username: email,
password: crypto.createHash('sha256').update(password).digest('base64') password: crypto.createHash('sha256').update(password).digest('base64')
}, },
type: userDB.Sequelize.QueryTypes.SELECT type: db.Sequelize.QueryTypes.SELECT
}) }).then(function(users) {
.then(function(users) {
if (users.length != 1) { if (users.length != 1) {
return null; return null;
} }
let user = users[0]; let user = users[0];
req.session.userId = user.id; req.session.userId = user.id;
return user; return user;
}) }).then(function(user) {
.then(function(user) {
if (!user) { if (!user) {
console.log(email + " not found (or invalid password.)"); console.log(email + ' not found (or invalid password.)');
req.session.userId = null; req.session.userId = null;
return res.status(401).send({ return res.status(401).send({
message: `Invalid sign in credentials` message: 'Invalid sign in credentials'
}); });
} }
let message = "Logged in as " + user.email + " (" + user.id + ")"; let message = 'Logged in as ' + user.email + ' (' + user.id + ')';
if (!user.mailVerified) { if (!user.mailVerified) {
console.log(message + ", who is not verified email."); console.log(message + ', who is not verified email.');
} else if (!user.authenticated) { } else if (!user.authenticated) {
console.log(message + ", who is not authenticated."); console.log(message + ', who is not authenticated.');
} else { } else {
console.log(message); console.log(message);
} }
@ -316,15 +313,14 @@ router.post("/signin", function(req, res) {
return getSessionUser(req).then(function(user) { return getSessionUser(req).then(function(user) {
return res.status(200).send(user); return res.status(200).send(user);
}); });
}) }).catch(function(error) {
.catch(function(error) {
console.log(error); console.log(error);
return res.status(403).send(error); return res.status(403).send(error);
}); });
}); });
router.post("/signout", (req, res) => { router.post('/signout', (req, res) => {
console.log("/users/signout"); console.log('/users/signout');
if (req.session && req.session.userId) { if (req.session && req.session.userId) {
req.session.userId = null; req.session.userId = null;
} }

View File

@ -1,14 +1,12 @@
"use strict";
function twoDigit(number) { function twoDigit(number) {
return ("0" + number).slice(-2); return ('0' + number).slice(-2);
} }
function timestamp(date) { function timestamp(date) {
date = date || new Date(); date = date || new Date();
return [ date.getFullYear(), twoDigit(date.getMonth() + 1), twoDigit(date.getDate()) ].join("-") + return [ date.getFullYear(), twoDigit(date.getMonth() + 1), twoDigit(date.getDate()) ].join('-') +
" " + ' ' +
[ twoDigit(date.getHours()), twoDigit(date.getMinutes()), twoDigit(date.getSeconds()) ].join(":"); [ twoDigit(date.getHours()), twoDigit(date.getMinutes()), twoDigit(date.getSeconds()) ].join(':');
} }
module.exports = { module.exports = {