Compare commits
2 Commits
e5ab93e092
...
d21946cb9e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d21946cb9e | ||
![]() |
c7435c4bb1 |
26
.eslintrc.json
Normal file
26
.eslintrc.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
@ -26,20 +26,43 @@ const App = () => {
|
||||
const [ csrfToken, setCsrfToken ] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
window.fetch(`${base}/api/v1/users/csrf`, {
|
||||
method: 'GET',
|
||||
cache: 'no-cache',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.json();
|
||||
}).then((data) => {
|
||||
const effect = async () => {
|
||||
const res = await window.fetch(`${base}/api/v1/users/csrf`, {
|
||||
method: 'GET',
|
||||
cache: 'no-cache',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
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 (
|
||||
<div className="App">
|
||||
<GlobalContext.Provider value={{user, setUser, csrfToken }}>
|
||||
|
16
server/.eslintrc.json
Normal file
16
server/.eslintrc.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"env": {
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
]
|
||||
}
|
||||
}
|
144
server/app.js
144
server/app.js
@ -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"),
|
||||
bodyParser = require("body-parser"),
|
||||
config = require("config"),
|
||||
const express = require('express'),
|
||||
bodyParser = require('body-parser'),
|
||||
config = require('config'),
|
||||
session = require('express-session'),
|
||||
basePath = require("./basepath"),
|
||||
cookieParser = require("cookie-parser"),
|
||||
basePath = require('./basepath'),
|
||||
cookieParser = require('cookie-parser'),
|
||||
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({
|
||||
secret: 'm@g1x!',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { secure: true }
|
||||
}));
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
app.use(cookieParser())
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(csrf({
|
||||
cookie: true
|
||||
}));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
//const ws = require('express-ws')(app, server);
|
||||
|
||||
require("./console-line.js"); /* Monkey-patch console.log with line numbers */
|
||||
|
||||
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) {
|
||||
app.use(function (err, req, res) {
|
||||
console.error(err.message);
|
||||
res.status(err.status || 500).json({
|
||||
message: err.message,
|
||||
@ -75,55 +43,47 @@ app.use(function(err, req, res, next) {
|
||||
});
|
||||
});
|
||||
|
||||
app.use(`${basePath}api/v1/groups`, require("./routes/groups"));
|
||||
app.use(`${basePath}api/v1/users`, require("./routes/users"));
|
||||
/* Initialize the data base, configure routes, and open server */
|
||||
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 */
|
||||
app.use(basePath, index);
|
||||
console.log('Hosting server from: ' + basePath);
|
||||
app.set('port', serverConfig.port);
|
||||
|
||||
/**
|
||||
* Create HTTP server and listen for new connections
|
||||
*/
|
||||
app.set("port", serverConfig.port);
|
||||
|
||||
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;
|
||||
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));
|
||||
});
|
||||
}).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) {
|
||||
if (error.syscall !== "listen") {
|
||||
throw error;
|
||||
}
|
||||
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");
|
||||
// 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");
|
||||
case 'EADDRINUSE':
|
||||
console.error(serverConfig.port + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
server.listen(serverConfig.port, () => {
|
||||
console.log(`http server listening on ${serverConfig.port}`);
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
let basePath = process.env.REACT_APP_basePath;
|
||||
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
|
||||
if (basePath == "//") {
|
||||
basePath = "/";
|
||||
basePath = '/' + basePath.replace(/^\/+/, '').replace(/\/+$/, '') + '/';
|
||||
if (basePath == '//') {
|
||||
basePath = '/';
|
||||
}
|
||||
|
||||
console.log(`Using basepath ${basePath}`);
|
||||
|
@ -1,15 +1,9 @@
|
||||
{
|
||||
"db": {
|
||||
"groups": {
|
||||
"goodtimes": {
|
||||
"dialect": "sqlite",
|
||||
"storage": "../db/groups.db",
|
||||
"logging" : false,
|
||||
"timezone": "+00:00"
|
||||
},
|
||||
"users": {
|
||||
"dialect": "sqlite",
|
||||
"storage": "../db/users.db",
|
||||
"logging" : false,
|
||||
"storage": "../db/goodtimes.db",
|
||||
"logging": false,
|
||||
"timezone": "+00:00"
|
||||
}
|
||||
},
|
||||
|
@ -1,8 +1,8 @@
|
||||
/* monkey-patch console.log to prefix with file/line-number */
|
||||
if (process.env.LOG_LINE) {
|
||||
let cwd = process.cwd(),
|
||||
cwdRe = new RegExp("^[^/]*" + cwd.replace("/", "\\/") + "\/([^:]*:[0-9]*).*$");
|
||||
[ "log", "warn", "error" ].forEach(function(method) {
|
||||
cwdRe = new RegExp('^[^/]*' + cwd.replace('/', '\\/') + '\/([^:]*:[0-9]*).*$');
|
||||
[ 'log', 'warn', 'error' ].forEach(function(method) {
|
||||
console[method] = (function () {
|
||||
let orig = console[method];
|
||||
return function () {
|
||||
@ -15,8 +15,8 @@ if (process.env.LOG_LINE) {
|
||||
}
|
||||
|
||||
let err = getErrorObject(),
|
||||
caller_line = err.stack.split("\n")[3],
|
||||
args = [caller_line.replace(cwdRe, "$1 -")];
|
||||
caller_line = err.stack.split('\n')[3],
|
||||
args = [caller_line.replace(cwdRe, '$1 -')];
|
||||
|
||||
/* arguments.unshift() doesn't exist... */
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
|
50
server/db/users.js → server/db.js
Executable file → Normal file
50
server/db/users.js → server/db.js
Executable file → Normal file
@ -1,15 +1,30 @@
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
const Sequelize = require('sequelize'),
|
||||
config = require('config');
|
||||
|
||||
function init() {
|
||||
const db = {
|
||||
sequelize: new Sequelize(config.get("db.users")),
|
||||
sequelize: new Sequelize(config.get('db.goodtimes')),
|
||||
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 User = db.sequelize.define('users', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
@ -32,6 +47,7 @@ function init() {
|
||||
timestamps: false
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const Authentication = db.sequelize.define('authentication', {
|
||||
key: {
|
||||
type: Sequelize.STRING,
|
||||
@ -41,7 +57,7 @@ function init() {
|
||||
issued: Sequelize.DATE,
|
||||
type: {
|
||||
type: Sequelize.ENUM,
|
||||
values: [ 'account-setup', 'password-reset' ]
|
||||
values: ['account-setup', 'password-reset']
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.INTEGER,
|
||||
@ -54,6 +70,28 @@ function init() {
|
||||
}, {
|
||||
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({
|
||||
force: false
|
||||
@ -61,11 +99,11 @@ function init() {
|
||||
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: Failed to authenticate with GROUP DB');
|
||||
console.log('ERROR: ' + JSON.stringify(config.get('db'), null, 2));
|
||||
console.log(error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = init();
|
||||
module.exports = { goodTimesDB: { init } };
|
@ -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();
|
@ -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
1401
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -38,5 +38,8 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.ketrenos.com:jketreno/goodtimes"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.12.0"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -1,30 +1,10 @@
|
||||
"use strict";
|
||||
|
||||
const express = require("express"),
|
||||
const express = require('express'),
|
||||
router = express.Router(),
|
||||
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;
|
||||
});
|
||||
crypto = require('crypto');
|
||||
|
||||
/* Simple NO-OP to set session cookie so user-id can use it as the
|
||||
* index */
|
||||
router.get("/", (req, res/*, next*/) => {
|
||||
router.get('/', (req, res/*, next*/) => {
|
||||
let userId;
|
||||
if (!req.cookies.user) {
|
||||
userId = crypto.randomBytes(16).toString('hex');
|
||||
@ -38,19 +18,19 @@ router.get("/", (req, res/*, next*/) => {
|
||||
return res.status(200).send({ user: userId });
|
||||
});
|
||||
|
||||
router.post("/", async (req, res/*, next*/) => {
|
||||
console.log(`POST /groups/`, req.session.userId);
|
||||
router.get('/', async (req, res/*, next*/) => {
|
||||
console.log('GET /groups/', req.session.userId);
|
||||
return res.status(200).send(
|
||||
[ {
|
||||
id: 1,
|
||||
ownerId: 1,
|
||||
name: "Beer Tuesday",
|
||||
name: 'Beer Tuesday',
|
||||
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;
|
||||
|
||||
let userId;
|
||||
@ -61,6 +41,10 @@ router.post("/:id", async (req, res/*, next*/) => {
|
||||
userId = req.cookies.user;
|
||||
}
|
||||
|
||||
const group = {
|
||||
id: 1
|
||||
};
|
||||
|
||||
if (id) {
|
||||
console.log(`[${userId.substring(0,8)}]: Attempting load of ${id}`);
|
||||
} else {
|
||||
|
@ -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;
|
@ -1,31 +1,26 @@
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
const express = require("express"),
|
||||
config = require("config"),
|
||||
{ sendVerifyMail, sendPasswordChangedMail, sendVerifiedMail } = require("../lib/mail"),
|
||||
crypto = require("crypto");
|
||||
const express = require('express'),
|
||||
{ sendVerifyMail, sendPasswordChangedMail, sendVerifiedMail } =
|
||||
require('../lib/mail'),
|
||||
crypto = require('crypto');
|
||||
|
||||
const router = express.Router();
|
||||
const autoAuthenticate = 1;
|
||||
|
||||
let userDB;
|
||||
|
||||
require("../db/users.js").then(function(db) {
|
||||
userDB = db;
|
||||
});
|
||||
|
||||
router.get("/", function(req, res/*, next*/) {
|
||||
console.log("/users/");
|
||||
router.get('/', function(req, res/*, next*/) {
|
||||
console.log('GET /users/');
|
||||
return getSessionUser(req).then((user) => {
|
||||
return res.status(200).send(user);
|
||||
}).catch((error) => {
|
||||
console.log("User not logged in: " + error);
|
||||
console.log('User not logged in: ' + error);
|
||||
return res.status(200).send({});
|
||||
});
|
||||
});
|
||||
|
||||
router.put("/password", function(req, res) {
|
||||
console.log("/users/password");
|
||||
router.put('/password', function(req, res) {
|
||||
console.log('/users/password');
|
||||
const db = req.app.locals.db;
|
||||
|
||||
const changes = {
|
||||
currentPassword: req.query.c || req.body.c,
|
||||
@ -33,21 +28,21 @@ router.put("/password", function(req, res) {
|
||||
};
|
||||
|
||||
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) {
|
||||
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 userDB.sequelize.query("SELECT id FROM users " +
|
||||
"WHERE uid=:username AND password=:password", {
|
||||
return db.sequelize.query('SELECT id FROM users ' +
|
||||
'WHERE uid=:username AND password=:password', {
|
||||
replacements: {
|
||||
username: user.username,
|
||||
password: crypto.createHash('sha256').update(changes.currentPassword).digest('base64')
|
||||
},
|
||||
type: userDB.Sequelize.QueryTypes.SELECT,
|
||||
type: db.Sequelize.QueryTypes.SELECT,
|
||||
raw: true
|
||||
}).then(function(users) {
|
||||
if (users.length != 1) {
|
||||
@ -57,34 +52,35 @@ router.put("/password", function(req, res) {
|
||||
});
|
||||
}).then(function(user) {
|
||||
if (!user) {
|
||||
console.log("Invalid password");
|
||||
console.log('Invalid password');
|
||||
/* Invalid password */
|
||||
res.status(401).send("Invalid password");
|
||||
res.status(401).send('Invalid password');
|
||||
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: {
|
||||
username: user.username,
|
||||
password: crypto.createHash('sha256').update(changes.newPassword).digest('base64')
|
||||
}
|
||||
}).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);
|
||||
user.id = req.session.userId;
|
||||
return sendPasswordChangedMail(userDB, req, user);
|
||||
return sendPasswordChangedMail(db, req, user);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/csrf", (req, res) => {
|
||||
console.log("/users/csrf");
|
||||
router.get('/csrf', (req, res) => {
|
||||
console.log('/users/csrf');
|
||||
res.json({ csrfToken: req.csrfToken() });
|
||||
});
|
||||
|
||||
router.post("/signup", function(req, res) {
|
||||
console.log("/users/signup");
|
||||
router.post('/signup', function(req, res) {
|
||||
console.log('/users/signup');
|
||||
const db = req.app.locals.db;
|
||||
|
||||
const user = {
|
||||
uid: req.body.email,
|
||||
@ -100,7 +96,7 @@ router.post("/signup", function(req, res) {
|
||||
|| !user.familyName
|
||||
|| !user.firstName) {
|
||||
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')
|
||||
.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,
|
||||
type: userDB.Sequelize.QueryTypes.SELECT,
|
||||
type: db.Sequelize.QueryTypes.SELECT,
|
||||
raw: true
|
||||
}).then(async function(results) {
|
||||
if (results.length != 0 && results[0].mailVerified) {
|
||||
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)) {
|
||||
const error = `Invalid email address: ${user.email}.`;
|
||||
console.log(error);
|
||||
@ -131,14 +127,14 @@ router.post("/signup", function(req, res) {
|
||||
|
||||
try {
|
||||
if (results.length != 0) {
|
||||
await userDB.sequelize.query("UPDATE users " +
|
||||
"SET mailVerified=0");
|
||||
await db.sequelize.query('UPDATE users ' +
|
||||
'SET mailVerified=0');
|
||||
req.session.userId = results[0].id;
|
||||
} else {
|
||||
let [, metadata] = await userDB.sequelize.query("INSERT INTO users " +
|
||||
"(uid,firstName,familyName,password,email,memberSince," +
|
||||
"authenticated,md5) " +
|
||||
`VALUES(:uid,:firstName,:familyName,:password,` +
|
||||
let [, metadata] = await db.sequelize.query('INSERT INTO users ' +
|
||||
'(uid,firstName,familyName,password,email,memberSince,' +
|
||||
'authenticated,md5) ' +
|
||||
'VALUES(:uid,:firstName,:familyName,:password,' +
|
||||
`:email,CURRENT_TIMESTAMP,${autoAuthenticate},:md5)`, {
|
||||
replacements: user
|
||||
});
|
||||
@ -147,7 +143,7 @@ router.post("/signup", function(req, res) {
|
||||
return getSessionUser(req).then(function(user) {
|
||||
res.status(200).send(user);
|
||||
user.id = req.session.userId;
|
||||
return sendVerifyMail(userDB, req, user);
|
||||
return sendVerifyMail(db, req, user);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -156,34 +152,35 @@ router.post("/signup", function(req, res) {
|
||||
});
|
||||
|
||||
const getSessionUser = function(req) {
|
||||
const db = req.app.locals.db;
|
||||
return Promise.resolve().then(function() {
|
||||
if (!req.session || !req.session.userId) {
|
||||
throw "Unauthorized. You must be logged in.";
|
||||
throw 'Unauthorized. You must be logged in.';
|
||||
}
|
||||
|
||||
let query = "SELECT " +
|
||||
"uid AS username,firstName,familyName,mailVerified,authenticated,memberSince,email,md5 " +
|
||||
"FROM users WHERE id=:id";
|
||||
return userDB.sequelize.query(query, {
|
||||
let query = 'SELECT ' +
|
||||
'uid AS username,firstName,familyName,mailVerified,authenticated,memberSince,email,md5 ' +
|
||||
'FROM users WHERE id=:id';
|
||||
return db.sequelize.query(query, {
|
||||
replacements: {
|
||||
id: req.session.userId
|
||||
},
|
||||
type: userDB.Sequelize.QueryTypes.SELECT,
|
||||
type: db.Sequelize.QueryTypes.SELECT,
|
||||
raw: true
|
||||
}).then(function(results) {
|
||||
if (results.length != 1) {
|
||||
throw "Invalid account.";
|
||||
throw 'Invalid account.';
|
||||
}
|
||||
|
||||
let user = results[0];
|
||||
|
||||
if (!user.mailVerified) {
|
||||
user.restriction = user.restriction || "Email address not verified.";
|
||||
user.restriction = user.restriction || 'Email address not verified.';
|
||||
return user;
|
||||
}
|
||||
|
||||
if (!user.authenticated) {
|
||||
user.restriction = user.restriction || "Accout not authorized.";
|
||||
user.restriction = user.restriction || 'Accout not authorized.';
|
||||
return user;
|
||||
}
|
||||
|
||||
@ -192,7 +189,7 @@ const getSessionUser = function(req) {
|
||||
}).then(function(user) {
|
||||
/* Strip out any fields that shouldn't be there. The allowed fields are: */
|
||||
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) {
|
||||
if (allowed.indexOf(field) == -1) {
|
||||
@ -201,114 +198,114 @@ const getSessionUser = function(req) {
|
||||
}
|
||||
return user;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
router.post("/verify-email", async (req, res) => {
|
||||
console.log("/users/verify-email");
|
||||
router.post('/verify-email', async (req, res) => {
|
||||
console.log('/users/verify-email');
|
||||
const key = req.body.token;
|
||||
const db = req.app.locals.db;
|
||||
|
||||
let results = await userDB.sequelize.query(
|
||||
"SELECT * FROM authentications WHERE key=:key", {
|
||||
replacements: { key },
|
||||
type: userDB.sequelize.QueryTypes.SELECT
|
||||
});
|
||||
let results = await db.sequelize.query(
|
||||
'SELECT * FROM authentications WHERE key=:key', {
|
||||
replacements: { key },
|
||||
type: db.sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
let token;
|
||||
if (results.length == 0) {
|
||||
console.log("Invalid key. Ignoring.");
|
||||
console.log('Invalid key. Ignoring.');
|
||||
return res.status(400).send({
|
||||
message: `Invalid authentication token.`
|
||||
message: 'Invalid authentication token.'
|
||||
});
|
||||
}
|
||||
|
||||
token = results[0];
|
||||
console.log(token);
|
||||
console.log("Matched token: " + JSON.stringify(token, null, 2));
|
||||
console.log(token);
|
||||
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: { key }
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return userDB.sequelize.query(
|
||||
"SELECT * FROM users WHERE id=:userId", {
|
||||
replacements: token,
|
||||
type: userDB.sequelize.QueryTypes.SELECT
|
||||
});
|
||||
})
|
||||
.then(function (results) {
|
||||
case 'account-setup':
|
||||
return db.sequelize.query(
|
||||
'UPDATE users SET mailVerified=1 WHERE id=:userId', {
|
||||
replacements: token
|
||||
}
|
||||
).then(function () {
|
||||
return db.sequelize.query(
|
||||
'DELETE FROM authentications WHERE key=:key', {
|
||||
replacements: { key }
|
||||
}
|
||||
);
|
||||
}).then(function () {
|
||||
return db.sequelize.query(
|
||||
'SELECT * FROM users WHERE id=:userId', {
|
||||
replacements: token,
|
||||
type: db.sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
}).then(function (results) {
|
||||
if (results.length == 0) {
|
||||
return res.status(500).send({
|
||||
message: `Internal authentication error.`
|
||||
message: 'Internal authentication error.'
|
||||
});
|
||||
}
|
||||
return results[0];
|
||||
})
|
||||
.then((user) => {
|
||||
sendVerifiedMail(userDB, req, user);
|
||||
}).then((user) => {
|
||||
sendVerifiedMail(db, req, user);
|
||||
req.session.userId = user.id;
|
||||
}).then(function (user) {
|
||||
return getSessionUser(req).then(function (user) {
|
||||
}).then(() => {
|
||||
return getSessionUser(req).then((user) => {
|
||||
return res.status(200).send(user);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/signin", function(req, res) {
|
||||
console.log("/users/signin");
|
||||
router.post('/signin', function(req, res) {
|
||||
console.log('/users/signin');
|
||||
const db = req.app.locals.db;
|
||||
|
||||
let { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
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 " +
|
||||
"id,mailVerified,authenticated," +
|
||||
"uid AS username," +
|
||||
"familyName,firstName,email " +
|
||||
"FROM users WHERE uid=:username AND password=:password";
|
||||
return userDB.sequelize.query(query, {
|
||||
let query = 'SELECT ' +
|
||||
'id,mailVerified,authenticated,' +
|
||||
'uid AS username,' +
|
||||
'familyName,firstName,email ' +
|
||||
'FROM users WHERE uid=:username AND password=:password';
|
||||
return db.sequelize.query(query, {
|
||||
replacements: {
|
||||
username: email,
|
||||
password: crypto.createHash('sha256').update(password).digest('base64')
|
||||
},
|
||||
type: userDB.Sequelize.QueryTypes.SELECT
|
||||
})
|
||||
.then(function(users) {
|
||||
type: db.Sequelize.QueryTypes.SELECT
|
||||
}).then(function(users) {
|
||||
if (users.length != 1) {
|
||||
return null;
|
||||
}
|
||||
let user = users[0];
|
||||
req.session.userId = user.id;
|
||||
return user;
|
||||
})
|
||||
.then(function(user) {
|
||||
}).then(function(user) {
|
||||
if (!user) {
|
||||
console.log(email + " not found (or invalid password.)");
|
||||
console.log(email + ' not found (or invalid password.)');
|
||||
req.session.userId = null;
|
||||
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) {
|
||||
console.log(message + ", who is not verified email.");
|
||||
console.log(message + ', who is not verified email.');
|
||||
} else if (!user.authenticated) {
|
||||
console.log(message + ", who is not authenticated.");
|
||||
console.log(message + ', who is not authenticated.');
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
@ -316,15 +313,14 @@ router.post("/signin", function(req, res) {
|
||||
return getSessionUser(req).then(function(user) {
|
||||
return res.status(200).send(user);
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
return res.status(403).send(error);
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/signout", (req, res) => {
|
||||
console.log("/users/signout");
|
||||
router.post('/signout', (req, res) => {
|
||||
console.log('/users/signout');
|
||||
if (req.session && req.session.userId) {
|
||||
req.session.userId = null;
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
function twoDigit(number) {
|
||||
return ("0" + number).slice(-2);
|
||||
return ('0' + number).slice(-2);
|
||||
}
|
||||
|
||||
function timestamp(date) {
|
||||
date = date || new Date();
|
||||
return [ date.getFullYear(), twoDigit(date.getMonth() + 1), twoDigit(date.getDate()) ].join("-") +
|
||||
" " +
|
||||
[ twoDigit(date.getHours()), twoDigit(date.getMinutes()), twoDigit(date.getSeconds()) ].join(":");
|
||||
return [ date.getFullYear(), twoDigit(date.getMonth() + 1), twoDigit(date.getDate()) ].join('-') +
|
||||
' ' +
|
||||
[ twoDigit(date.getHours()), twoDigit(date.getMinutes()), twoDigit(date.getSeconds()) ].join(':');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user