1
0

Auth is working again with express-session for user tracking

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-04-07 16:36:56 -07:00
parent d21946cb9e
commit 4c6040e3bc
11 changed files with 285 additions and 132 deletions

1
.env
View File

@ -1,2 +1,3 @@
REACT_APP_basePath="/"
NODE_CONFIG_ENV='production'
LOG_LINE=1

View File

@ -2,3 +2,4 @@ PORT=3001
PUBLIC_URL=/
HOST=nuc.ketrenos.com
DANGEROUSLY_DISABLE_HOST_CHECK='true'
LOG_LINE=1

View File

@ -24,6 +24,7 @@ import { base } from "./Common.js";
const App = () => {
const [ user, setUser ] = useState(null);
const [ csrfToken, setCsrfToken ] = useState(undefined);
const [ loading, setLoading ] = useState(true);
useEffect(() => {
const effect = async () => {
@ -43,7 +44,7 @@ const App = () => {
}, []);
useEffect(() => {
if (csrfToken) {
if (!csrfToken) {
return;
}
const effect = async () => {
@ -57,6 +58,12 @@ const App = () => {
},
});
const data = await res.json();
setLoading(false);
if (res.status >= 400) {
console.log(data.message ? data.message : res.statusText);
return;
}
setUser(data);
};
@ -83,9 +90,12 @@ const App = () => {
<Route path="/" element={
<Paper style={{padding: "0.5rem"}}>You need to verify your email via the link sent to {user.email}.</Paper>}/>
}
{ !user &&
{ !user && !loading &&
<Route path="/" element={<SignIn />} />
}
{ !user && loading &&
<Route path="/" element={<div>Loading...</div>} />
}
</Routes>
</Container>
</GlobalContext.Provider>

View File

@ -23,7 +23,7 @@ function Dashboard() {
const effect = async () => {
const res = await window.fetch(
`${base}/api/v1/groups`, {
method: 'POST',
method: 'GET',
cache: 'no-cache',
credentials: 'same-origin',
headers: {

View File

@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react';
import React, { useContext, useState, useCallback } from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
@ -37,10 +37,10 @@ export default function SignIn() {
const [ error, setError ] = useState(undefined);
const navigate = useNavigate();
const handleSubmit = (event) => {
const handleSubmit = useCallback(async (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
window.fetch(`${base}/api/v1/users/signin`, {
const form = new FormData(event.currentTarget);
const res = await window.fetch(`${base}/api/v1/users/signin`, {
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
@ -49,28 +49,22 @@ export default function SignIn() {
'CSRF-Token': csrfToken
},
body: JSON.stringify({
email: data.get('email'),
password: data.get('password')
email: form.get('email'),
password: form.get('password')
})
})
.then(async (res) => {
return [ res.status, res.statusText, await res.json() ];
})
.then(([ status, statusText, data ]) => {
if (status >= 400) {
setError(data.message ? data.message : statusText);
return null;
} if (!data) {
return;
}
setUser(data);
navigate("/");
})
.catch((error) => {
console.error(`Error: `, error);
setError(`Error authenticating.`);
});
};
const data = await res.json();
if (res.status >= 400) {
setError(data.message ? data.message : res.statusText);
return null;
}
if (!data) {
return;
}
setUser(data);
navigate("/");
}, [csrfToken, navigate, setUser ]);
return (
<ThemeProvider theme={theme}>

View File

@ -11,6 +11,9 @@
"indent": [
"error",
2
]
}
],
"quotes": [ 2, "single", {
"allowTemplateLiterals": true
} ]
}
}

View File

@ -8,12 +8,16 @@ const express = require('express'),
bodyParser = require('body-parser'),
config = require('config'),
session = require('express-session'),
sqliteStoreFactory = require('express-session-sqlite').default,
basePath = require('./basepath'),
cookieParser = require('cookie-parser'),
app = express(),
csrf = require('csurf'),
http = require('http'),
{ goodTimesDB } = require('./db.js');
methodOverride = require('method-override'),
cookieParser = require('cookie-parser'),
{ goodTimesDB } = require('./db.js'),
sqlite3 = require('sqlite3'),
SqliteStore = sqliteStoreFactory(session);
require('./console-line.js'); /* Monkey-patch console.log with line numbers */
@ -21,12 +25,27 @@ require('./console-line.js'); /* Monkey-patch console.log with line numbers */
* set in the headers */
app.set('trust proxy', true);
app.use(session({
secret: 'm@g1x!',
resave: false,
secret: 'm@g1x!c00k13$',
maxAge: 14 * 24 * 60 * 60 * 1000, /* 2 weeks */
resave: true,
saveUninitialized: false,
cookie: { secure: true }
cookie: { secure: true },
store: new SqliteStore({
driver: sqlite3.Database,
path: config.get('sessions.db'),
ttl: 5000,
cleanupInterval: 300000
}),
}));
app.use(bodyParser.urlencoded({ extended: false }));
app.use((req, res, next) => {
console.log('CSRF debug: ', {
token: req.header('CSRF-Token'),
method: req.method
});
next();
});
app.use(cookieParser());
app.use(csrf({
cookie: true
@ -35,22 +54,30 @@ app.use(bodyParser.json());
//const ws = require('express-ws')(app, server);
app.use(function (err, req, res) {
console.error(err.message);
res.status(err.status || 500).json({
message: err.message,
error: {}
});
});
/* 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'));
/* Error handler and catch for 404 */
app.use(methodOverride());
app.use((err, req, res, next) => {
console.log('CSRF debug: ', {
token: req.header('CSRF-Token'),
csrf: req.csrfToken(),
method: req.method
});
console.error(err);
res.status(err.status || 500).json({
message: err.message,
error: {}
});
});
}).then(() => {
const server = http.createServer(app),
serverConfig = config.get('server');

126
server/package-lock.json generated
View File

@ -18,9 +18,11 @@
"csurf": "^1.11.0",
"express": "^4.17.3",
"express-session": "^1.17.1",
"express-session-sqlite": "^2.0.10",
"express-ws": "^5.0.2",
"fast-deep-equal": "^3.1.3",
"handlebars": "^4.7.7",
"method-override": "^3.0.0",
"moment": "^2.24.0",
"morgan": "^1.9.1",
"node-fetch": "^2.6.0",
@ -31,7 +33,7 @@
"pluralize": "^8.0.0",
"random-words": "^1.1.2",
"sequelize": "^5.21.6",
"sqlite3": "^4.1.1",
"sqlite3": "^4.2.0",
"typeface-roboto": "0.0.75",
"ws": "^8.5.0"
},
@ -1135,6 +1137,37 @@
"node": ">= 0.8.0"
}
},
"node_modules/express-session-sqlite": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/express-session-sqlite/-/express-session-sqlite-2.0.10.tgz",
"integrity": "sha512-VBNvSlVUqAHHYU4Cca4Q1waJe1CeW2VFlW/PDtGY/cyJUV5Zy+qkN4A18SNvNR//SDZO+Pc3qd/AgVVQw2/W2Q==",
"dependencies": {
"debug": "^4.2.0",
"sql-template-strings": "^2.2.2",
"sqlite": "^4.0.14"
}
},
"node_modules/express-session-sqlite/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/express-session-sqlite/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/express-ws": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz",
@ -1990,6 +2023,28 @@
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"node_modules/method-override": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz",
"integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==",
"dependencies": {
"debug": "3.1.0",
"methods": "~1.1.2",
"parseurl": "~1.3.2",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/method-override/node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@ -3003,6 +3058,19 @@
"node": ">=0.10.0"
}
},
"node_modules/sql-template-strings": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/sql-template-strings/-/sql-template-strings-2.2.2.tgz",
"integrity": "sha1-PxFQiiWt384hejBCqdMAwxk7lv8=",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/sqlite": {
"version": "4.0.25",
"resolved": "https://registry.npmjs.org/sqlite/-/sqlite-4.0.25.tgz",
"integrity": "sha512-gqCEcLF8FOTeW/na3SRYWLQkw2jZXgVj1DdgRJbm0jvrhnUgBIuNDUUm649AnBNDNHhI5XskwT8dvc8vearRLQ=="
},
"node_modules/sqlite3": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz",
@ -4330,6 +4398,31 @@
"uid-safe": "~2.1.5"
}
},
"express-session-sqlite": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/express-session-sqlite/-/express-session-sqlite-2.0.10.tgz",
"integrity": "sha512-VBNvSlVUqAHHYU4Cca4Q1waJe1CeW2VFlW/PDtGY/cyJUV5Zy+qkN4A18SNvNR//SDZO+Pc3qd/AgVVQw2/W2Q==",
"requires": {
"debug": "^4.2.0",
"sql-template-strings": "^2.2.2",
"sqlite": "^4.0.14"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"express-ws": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz",
@ -4939,6 +5032,27 @@
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"method-override": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz",
"integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==",
"requires": {
"debug": "3.1.0",
"methods": "~1.1.2",
"parseurl": "~1.3.2",
"vary": "~1.1.2"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@ -5728,6 +5842,16 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"sql-template-strings": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/sql-template-strings/-/sql-template-strings-2.2.2.tgz",
"integrity": "sha1-PxFQiiWt384hejBCqdMAwxk7lv8="
},
"sqlite": {
"version": "4.0.25",
"resolved": "https://registry.npmjs.org/sqlite/-/sqlite-4.0.25.tgz",
"integrity": "sha512-gqCEcLF8FOTeW/na3SRYWLQkw2jZXgVj1DdgRJbm0jvrhnUgBIuNDUUm649AnBNDNHhI5XskwT8dvc8vearRLQ=="
},
"sqlite3": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz",

View File

@ -18,9 +18,11 @@
"csurf": "^1.11.0",
"express": "^4.17.3",
"express-session": "^1.17.1",
"express-session-sqlite": "^2.0.10",
"express-ws": "^5.0.2",
"fast-deep-equal": "^3.1.3",
"handlebars": "^4.7.7",
"method-override": "^3.0.0",
"moment": "^2.24.0",
"morgan": "^1.9.1",
"node-fetch": "^2.6.0",
@ -31,7 +33,7 @@
"pluralize": "^8.0.0",
"random-words": "^1.1.2",
"sequelize": "^5.21.6",
"sqlite3": "^4.1.1",
"sqlite3": "^4.2.0",
"typeface-roboto": "0.0.75",
"ws": "^8.5.0"
},

View File

@ -2,22 +2,6 @@ const express = require('express'),
router = express.Router(),
crypto = require('crypto');
/* Simple NO-OP to set session cookie so user-id can use it as the
* index */
router.get('/', (req, res/*, next*/) => {
let userId;
if (!req.cookies.user) {
userId = crypto.randomBytes(16).toString('hex');
res.cookie('user', userId);
} else {
userId = req.cookies.user;
}
console.log(`[${userId.substring(0, 8)}]: Browser hand-shake achieved.`);
return res.status(200).send({ user: userId });
});
router.get('/', async (req, res/*, next*/) => {
console.log('GET /groups/', req.session.userId);
return res.status(200).send(

View File

@ -8,17 +8,18 @@ const express = require('express'),
const router = express.Router();
const autoAuthenticate = 1;
router.get('/', function(req, res/*, next*/) {
router.get('/', (req, res/*, next*/) => {
console.log('GET /users/');
return getSessionUser(req).then((user) => {
if (typeof user === 'string') {
return res.status(403).send({ message: user });
}
return res.status(200).send(user);
}).catch((error) => {
console.log('User not logged in: ' + error);
return res.status(200).send({});
});
});
router.put('/password', function(req, res) {
router.put('/password', async (req, res) => {
console.log('/users/password');
const db = req.app.locals.db;
@ -28,28 +29,32 @@ 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 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: db.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(users) {
if (users.length != 1) {
return null;
}
return user;
});
const user = await getSessionUser(req);
if (typeof user === 'string') {
return res.status(403).send({ message: user });
}
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: db.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(users) {
if (users.length != 1) {
return null;
}
return user;
}).then(function(user) {
if (!user) {
console.log('Invalid password');
@ -74,8 +79,9 @@ router.put('/password', function(req, res) {
});
router.get('/csrf', (req, res) => {
console.log('/users/csrf');
res.json({ csrfToken: req.csrfToken() });
const token = req.csrfToken();
console.log('/users/csrf', token);
res.json({ csrfToken: token });
});
router.post('/signup', function(req, res) {
@ -141,6 +147,9 @@ router.post('/signup', function(req, res) {
req.session.userId = metadata.lastID;
}
return getSessionUser(req).then(function(user) {
if (typeof user === 'string') {
return res.status(403).send({ message: user });
}
res.status(200).send(user);
user.id = req.session.userId;
return sendVerifyMail(db, req, user);
@ -151,53 +160,48 @@ router.post('/signup', function(req, res) {
});
});
const getSessionUser = function(req) {
const getSessionUser = async (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.';
}
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: db.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(results) {
if (results.length != 1) {
throw 'Invalid account.';
}
console.log(req.session);
if (!req.session || !req.session.userId) {
return 'Unauthorized. You must be logged in.';
}
let user = results[0];
if (!user.mailVerified) {
user.restriction = user.restriction || 'Email address not verified.';
return user;
}
if (!user.authenticated) {
user.restriction = user.restriction || 'Accout not authorized.';
return user;
}
return user;
});
}).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'
];
for (let field in user) {
if (allowed.indexOf(field) == -1) {
delete user[field];
}
}
return user;
let query = 'SELECT ' +
'uid AS username,firstName,familyName,mailVerified,authenticated,memberSince,email,md5 ' +
'FROM users WHERE id=:id';
const results = await db.sequelize.query(query, {
replacements: {
id: req.session.userId
},
type: db.Sequelize.QueryTypes.SELECT,
raw: true
});
if (results.length != 1) {
return 'Invalid account.';
}
const user = results[0];
if (!user.mailVerified) {
user.restriction = user.restriction || 'Email address not verified.';
} else if (!user.authenticated) {
user.restriction = user.restriction || 'Accout not authorized.';
}
/* Strip out any fields that shouldn't be there.
* The allowed fields are: */
const allowed = [
'maintainer', 'username', 'firstName', 'familyName', 'mailVerified', 'authenticated', 'name', 'email', 'restriction', 'md5'
];
for (let field in user) {
if (allowed.indexOf(field) == -1) {
delete user[field];
}
}
return user;
};
router.post('/verify-email', async (req, res) => {
@ -254,13 +258,16 @@ router.post('/verify-email', async (req, res) => {
req.session.userId = user.id;
}).then(() => {
return getSessionUser(req).then((user) => {
if (typeof user === 'string') {
return res.status(403).send({ message: user });
}
return res.status(200).send(user);
});
});
}
});
router.post('/signin', function(req, res) {
router.post('/signin', (req, res) => {
console.log('/users/signin');
const db = req.app.locals.db;
@ -310,12 +317,12 @@ router.post('/signin', function(req, res) {
console.log(message);
}
return getSessionUser(req).then(function(user) {
return getSessionUser(req).then((user) => {
if (typeof user === 'string') {
return res.status(403).send({ message: user });
}
return res.status(200).send(user);
});
}).catch(function(error) {
console.log(error);
return res.status(403).send(error);
});
});