spring cleaning

pull/4/head
Cameron Clough 2022-03-20 23:59:37 +00:00
parent 43a71d3aad
commit 5e6d0dbcf5
No known key found for this signature in database
GPG Key ID: BFB3B74B026ED43F
26 changed files with 1680 additions and 1875 deletions

View File

@ -8,6 +8,7 @@ DB_HOST=db # If using docker compose, this should match the container service na
DB_PORT=5432
DB_FORCE_SYNC=false # Whether or not to DROP all tables and recreate to match the current models
ALLOW_REGISTRATION=true
AUTH_2FA_ISSUER=RetroPilot
HTTP_INTERFACE=0.0.0.0
HTTP_PORT=3000
CAN_SEND_MAIL=true # Set to false to skip sending mail, all attempted mail is logged under DEBUG

View File

@ -5,12 +5,17 @@
"env": {
"es6": true,
"node": true,
"browser": false // do we need this? none of this code will ever run on a browswer
"browser": false // do we need this? none of this code will ever run on a browswer
},
"plugins": [
"no-floating-promise"
],
"rules": {
// enforces no braces where they can be omitted
// https://eslint.org/docs/rules/arrow-body-style
// retropilot: this seems dumb. do whatever looks nice.
"arrow-body-style": "off",
// disallow use of unary operators, ++ and --
// http://eslint.org/docs/rules/no-plusplus
// retropilot: we allow them in the for loop

View File

@ -6,10 +6,9 @@ WORKDIR /app
# Install app dependencies
COPY package*.json ./
RUN npm ci
RUN npm install pm2 -g
# Bundle app source
COPY . .
EXPOSE 3000
CMD ["pm2-runtime", "ecosystem.config.js"]
CMD ["node", "-r", "esm", "server.js"]

View File

@ -1,119 +0,0 @@
import crypto from 'crypto';
import jsonwebtoken from 'jsonwebtoken';
import log4js from 'log4js';
import orm from '../models/index.model';
const logger = log4js.getLogger('default');
async function validateJWT(token, key) {
try {
return jsonwebtoken.verify(token.replace('JWT ', ''), key, { algorithms: ['RS256'], ignoreNotBefore: true });
} catch (exception) {
logger.warn(`failed to validate JWT ${exception}`);
}
return null;
}
async function readJWT(token) {
try {
return jsonwebtoken.decode(token);
} catch (exception) {
logger.warn(`failed to read JWT ${exception}`);
}
return null;
}
async function signIn(email, password) {
let account = await orm.models.accounts.findOne({ where: { email } });
if (account && account.dataValues) {
account = account.dataValues;
const inputPassword = crypto.createHash('sha256').update(password + process.env.APP_SALT).digest('hex');
if (account.password === inputPassword) {
const token = jsonwebtoken.sign({ accountId: account.id }, process.env.APP_SALT);
return { success: true, jwt: token };
}
return { success: false, msg: 'BAD PASSWORD', invalidPassword: true };
}
return { success: false, msg: 'BAD ACCOUNT', badAccount: true };
}
async function changePassword(account, newPassword, oldPassword) {
if (!account || !newPassword || !oldPassword) {
return { success: false, error: 'MISSING_DATA' };
}
const oldPasswordHash = crypto.createHash('sha256').update(oldPassword + process.env.APP_SALT).digest('hex');
if (account.password === oldPasswordHash) {
const newPasswordHash = crypto.createHash('sha256').update(newPassword + process.env.APP_SALT).digest('hex');
await orm.models.accounts.update(
{ password: newPasswordHash },
{ where: { id: account.id } },
);
return { success: true, msg: 'PASSWORD CHANGED', changed: true };
}
return { success: false, msg: 'BAD PASSWORD', passwordCorrect: false };
}
/*
TODO: update rest of the code to support authentication rejection reasons
*/
async function getAuthenticatedAccount(req) {
const sessionJWT = req.cookies.jwt;
if ((!sessionJWT || sessionJWT.expires <= Date.now())) {
return null;
}
return getAccountFromJWT(sessionJWT);
}
async function getAccountFromJWT(jwt, limitData) {
let token;
try {
token = jsonwebtoken.verify(jwt, process.env.APP_SALT);
} catch (err) {
return null;// {success: false, msg: 'BAD_JWT'}
}
if (!token || !token.accountId) {
return null; // {success: false, badToken: true}
}
let query = { where: { id: token.accountId } };
if (limitData) {
query = { ...query, attributes: { exclude: ['password', '2fa_token', 'session_seed'] } };
}
const account = await orm.models.accounts.findOne(query);
if (!account.dataValues) {
return null; // {success: false, isInvalid: true}
}
try {
await orm.models.accounts.update(
{ last_ping: Date.now() },
{ where: { id: account.id } },
);
} catch(error) {
console.log(error);
}
if (!account || account.banned) {
return null; // {success: false, isBanned: true}
}
return account;
}
export default {
validateJWT,
getAuthenticatedAccount,
changePassword,
signIn,
readJWT,
getAccountFromJWT,
};

View File

@ -0,0 +1,119 @@
import crypto from 'crypto';
import jsonwebtoken from 'jsonwebtoken';
import log4js from 'log4js';
import orm from '../../models/index.model';
const logger = log4js.getLogger('default');
export async function validateJWT(token, key) {
try {
return jsonwebtoken.verify(token.replace('JWT ', ''), key, { algorithms: ['RS256'], ignoreNotBefore: true });
} catch (exception) {
logger.warn(`failed to validate JWT ${exception}`);
}
return null;
}
export async function readJWT(token) {
try {
return jsonwebtoken.decode(token);
} catch (exception) {
logger.warn(`failed to read JWT ${exception}`);
}
return null;
}
async function signIn(email, password) {
let account = await orm.models.accounts.findOne({ where: { email } });
if (account && account.dataValues) {
account = account.dataValues;
const inputPassword = crypto.createHash('sha256').update(password + process.env.APP_SALT).digest('hex');
if (account.password === inputPassword) {
const token = jsonwebtoken.sign({ accountId: account.id }, process.env.APP_SALT);
return { success: true, jwt: token };
}
return { success: false, msg: 'BAD PASSWORD', invalidPassword: true };
}
return { success: false, msg: 'BAD ACCOUNT', badAccount: true };
}
async function changePassword(account, newPassword, oldPassword) {
if (!account || !newPassword || !oldPassword) {
return { success: false, error: 'MISSING_DATA' };
}
const oldPasswordHash = crypto.createHash('sha256').update(oldPassword + process.env.APP_SALT).digest('hex');
if (account.password === oldPasswordHash) {
const newPasswordHash = crypto.createHash('sha256').update(newPassword + process.env.APP_SALT).digest('hex');
await orm.models.accounts.update(
{ password: newPasswordHash },
{ where: { id: account.id } },
);
return { success: true, msg: 'PASSWORD CHANGED', changed: true };
}
return { success: false, msg: 'BAD PASSWORD', passwordCorrect: false };
}
/*
TODO: update rest of the code to support authentication rejection reasons
*/
async function getAuthenticatedAccount(req) {
const sessionJWT = req.cookies.jwt;
if ((!sessionJWT || sessionJWT.expires <= Date.now())) {
return null;
}
return getAccountFromJWT(sessionJWT);
}
async function getAccountFromJWT(jwt, limitData) {
let token;
try {
token = jsonwebtoken.verify(jwt, process.env.APP_SALT);
} catch (err) {
return null;// {success: false, msg: 'BAD_JWT'}
}
if (!token || !token.accountId) {
return null; // {success: false, badToken: true}
}
let query = { where: { id: token.accountId } };
if (limitData) {
query = { ...query, attributes: { exclude: ['password', '2fa_token', 'session_seed'] } };
}
const account = await orm.models.accounts.findOne(query);
if (!account.dataValues) {
return null; // {success: false, isInvalid: true}
}
try {
await orm.models.accounts.update(
{ last_ping: Date.now() },
{ where: { id: account.id } },
);
} catch (error) {
console.log(error);
}
if (!account || account.banned) {
return null; // {success: false, isBanned: true}
}
return account;
}
export default {
validateJWT,
getAuthenticatedAccount,
changePassword,
signIn,
readJWT,
getAccountFromJWT,
};

View File

@ -1 +1 @@
//empty
// empty

View File

@ -7,6 +7,9 @@ export function oauthRegister(email) {
const account = getAccountFromEmail(email);
if (account) return { error: true, ...AUTH_REGISTER_ALREADY_REGISTERED };
// TODO: finish
return { error: false };
}
export default function register() {

View File

@ -12,7 +12,7 @@ export async function twoFactorOnboard(account) {
if (!account || !account.dataValues) { return { success: false, ...AUTH_2FA_BAD_ACCOUNT }; }
if (account['2fa_token'] !== null) return { success: false, ...AUTH_2FA_ONBOARD_ALREADY_ENROLLED };
const token = await generateSecret(account.email, config.enterprise.name);
const token = await generateSecret(account.email, process.env.AUTH_2FA_ISSUER);
orm.models.account.update(
{ '2fa_token': token.secret },

View File

@ -2,9 +2,10 @@ import sanitizeFactory from 'sanitize';
import crypto from 'crypto';
import dirTree from 'directory-tree';
import log4js from 'log4js';
import authenticationController from './authentication';
import orm from '../models/index.model';
import usersController from './users';
import { readJWT, validateJWT } from './authentication';
import { getAccountFromId } from './users';
const logger = log4js.getLogger('default');
const sanitize = sanitizeFactory();
@ -25,7 +26,7 @@ async function pairDevice(account, qrString) {
device = await orm.models.device.findOne({ where: { serial } });
pairJWT = pairToken;
} else {
const data = await authenticationController.readJWT(qrString);
const data = await readJWT(qrString);
if (!data || !data.pair) {
return { success: false, noPair: true };
}
@ -33,11 +34,11 @@ async function pairDevice(account, qrString) {
pairJWT = qrString;
}
if (deviceQuery == null || !deviceQuery.dataValues) {
if (device == null || !device.dataValues) {
return { success: false, registered: false, noPair: true };
}
const decoded = await authenticationController.validateJWT(pairJWT, device.public_key);
const decoded = await validateJWT(pairJWT, device.public_key);
if (decoded == null || !decoded.pair) {
return { success: false, badToken: true };
}
@ -151,7 +152,7 @@ async function isUserAuthorised(accountId, dongleId) {
return { success: false, msg: 'bad_data' };
}
const account = await usersController.getAccountFromId(accountId);
const account = await getAccountFromId(accountId);
if (!account || !account.dataValues) {
return { success: false, msg: 'bad_account', data: { authorised: false, account_id: accountId } };
}

View File

@ -35,7 +35,7 @@ function simpleStringify(object) {
}
function formatDate(timestampMs) {
return new Date(parseInt(timestampMs)).toISOString().replace(/T/, ' ').replace(/\..+/, '');
return new Date(parseInt(timestampMs, 10)).toISOString().replace(/T/, ' ').replace(/\..+/, '');
}
export default {

View File

@ -6,6 +6,7 @@ import mailing from './mailing';
import users from './users';
import admin from './admin';
import devices from './devices';
// TO DO, finish up removing this callback stuff
export default {
authentication,

View File

@ -3,6 +3,7 @@ import log4js from 'log4js';
import orm from '../models/index.model';
const logger = log4js.getLogger('default');
export async function getAccountFromId(id) {
return orm.models.accounts.findByPk(id);
}

View File

@ -10,6 +10,14 @@ services:
ports:
- "3000:3000"
- "4040:4040"
worker:
build: .
command: node -r esm worker.js
restart: unless-stopped
depends_on:
- db
volumes:
- ./:/app
db:
image: postgres:14-bullseye
restart: always

View File

@ -1,7 +1,8 @@
import { DataTypes } from 'sequelize';
export default (sequelize) => {
sequelize.define('athena_returned_data',
sequelize.define(
'athena_returned_data',
{
id: {
allowNull: false,

View File

@ -2,14 +2,6 @@
/* eslint-disable global-require */
import { Sequelize } from 'sequelize';
import devices from './devices.model';
import drives from './drives.model';
import accounts from './accounts.model';
import athena_action_log from './athena_action_log.model';
import athena_returned_data from './athena_returned_data.model';
import device_authorised_users from './device_authorised_users.model';
import drive_segments from './drive_segments.model';
import oauth_accounts from './oauth_accounts';
const sequelize = new Sequelize({
username: process.env.DB_USER,
@ -23,14 +15,14 @@ const sequelize = new Sequelize({
sequelize.options.logging = () => {};
const modelDefiners = [
devices,
drives,
accounts,
athena_action_log,
athena_returned_data,
device_authorised_users,
drive_segments,
oauth_accounts,
require('./devices.model').default,
require('./drives.model').default,
require('./accounts.model').default,
require('./athena_action_log.model').default,
require('./athena_returned_data.model').default,
require('./device_authorised_users.model').default,
require('./drive_segments.model').default,
require('./oauth_accounts.model').default,
];
for (const modelDefiner of modelDefiners) {

3181
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
import express from 'express';
import log4js from 'log4js';
import { getURL, getToken } from '../../../controllers/authentication/oauth/google';
import authenticationController from '../../../controllers/authentication';
import log4js from 'log4js';
const router = express.Router();
const logger = log4js.getLogger('default');

View File

@ -22,7 +22,7 @@ async function isAuthenticated(req, res, next) {
}
router.get('/authentication/twofactor/enrol', isAuthenticated, async (req, res) => {
// TODO: implementation
});
export default router;

View File

@ -1,16 +1,17 @@
import express from 'express';
import bodyParser from 'body-parser';
import crypto from 'crypto';
import dirTree from 'directory-tree';
import bodyParser from 'body-parser';
import deviceSchema from '../../schema/routes/devices';
import express from 'express';
import log4js from 'log4js';
import deviceController from '../../controllers/devices';
import authenticationController from '../../controllers/authentication';
import deviceController from '../../controllers/devices';
import { MutateDevice } from '../../schema/routes/devices';
const logger = log4js.getLogger('default');
const router = express.Router();
async function isAuthenticated(req, res, next) {
const account = await authenticationController.getAuthenticatedAccount(req);
@ -34,18 +35,17 @@ router.get('/retropilot/0/devices', isAuthenticated, async (req, res) => {
/*
{
version: "1.0"
2fa: {
tokenProvided: false,
token: 000000
unixTime: 00000
},
modifications: {
nicname: x
publicKey: x
}
version: "1.0"
2fa: {
tokenProvided: false,
token: 000000
unixTime: 00000
},
modifications: {
nicname: x
publicKey: x
}
}
*/
router.put('/retropilot/0/device/:dongle_id/', [isAuthenticated, bodyParser.json()], async (req, res) => {
@ -54,7 +54,9 @@ router.put('/retropilot/0/device/:dongle_id/', [isAuthenticated, bodyParser.json
}
const { body } = req;
logger.log(deviceSchema.MutateDevice.isValid(body));
logger.log(MutateDevice.isValid(body));
// TODO: response?
return res.json({ success: true });
});
router.get('/retropilot/0/device/:dongle_id/drives/:drive_identifier/segment', isAuthenticated, async (req, res) => {

View File

@ -1,7 +1,7 @@
import bodyParser from 'body-parser';
import express from 'express';
import userController from '../../controllers/users';
import { createAccount, verifyEmailToken } from '../../controllers/users';
const router = express.Router();
router.post('/retropilot/0/register/email', bodyParser.urlencoded({ extended: true }), async (req, res) => {
@ -12,7 +12,7 @@ router.post('/retropilot/0/register/email', bodyParser.urlencoded({ extended: tr
return res.status(400).json({ success: false, msg: 'malformed request' });
}
const accountStatus = await userController.createAccount(req.body.email, req.body.password);
const accountStatus = await createAccount(req.body.email, req.body.password);
if (accountStatus && accountStatus.status) {
return res.status(accountStatus.status).json(accountStatus);
}
@ -25,7 +25,7 @@ router.get('/retropilot/0/register/verify/:token', bodyParser.urlencoded({ exten
return res.status(400).json({ success: false, status: 400, data: { missingToken: true } });
}
const verified = await userController.verifyEmailToken(req.params.token);
const verified = await verifyEmailToken(req.params.token);
if (verified && verified.status) {
return res.status(verified.status).json(verified);

View File

@ -1,3 +1,5 @@
/* eslint-disable */
// TODO: delete useradmin...
import express from 'express';
import bodyParser from 'body-parser';
import crypto from 'crypto';
@ -517,14 +519,14 @@ router.get('/useradmin/drive/:dongleId/:driveIdentifier', runAsyncWrapper(async
<a id="show-button" href="#" onclick="
document.getElementById('hide-button').style.display = 'inline';
document.getElementById('show-button').style.display = 'none';
document.getElementById('car-parameter-div').style.display = 'block';
document.getElementById('car-parameter-div').style.display = 'block';
return false;">Show</a>
<a id="hide-button" style="display: none;" href="#" onclick="
document.getElementById('hide-button').style.display = 'none';
document.getElementById('show-button').style.display = 'inline';
document.getElementById('car-parameter-div').style.display = 'none';
document.getElementById('car-parameter-div').style.display = 'none';
return false;">Hide</a>
<br><pre id="car-parameter-div" style="display: none; font-size: 0.8em">${carParams}</pre>
<br>

View File

@ -9,7 +9,6 @@ export const MutateDevice = yup.object().shape({
},
});
export default {
};

View File

@ -120,7 +120,7 @@ lockfile.lock('retropilot_server', { realpath: false, stale: 30000, update: 2000
const httpServer = http.createServer(app);
httpServer.listen(process.env.HTTP_PORT, () => {
logger.info(`Retropilot Server listening at http://${process.env.BASE_URL}`);
logger.info(`RetroPilot Server listening at ${process.env.BASE_URL}`);
});
}).catch((e) => {

View File

@ -1,9 +1,9 @@
/* eslint-disable no-underscore-dangle */
let wss;
import { v4 as uuid } from 'uuid';
import orm from '../../models/index.model';
let realtime;
let wss;
async function incoming(ws, res, msg) {
return realtime.passData(ws.dongleId, msg);

View File

@ -2,11 +2,11 @@ import { WebSocketServer } from 'ws';
import cookie from 'cookie';
import httpServer from 'http';
import log4js from 'log4js';
import controlsFunction from './controls.js';
import authenticationController from '../../controllers/authentication.js';
import authenticationController from '../../controllers/authentication';
import athenaRealtime from '../athena';
import athenaRealtime from '../athena/index.js';
import realtimeCommands from './commands.js';
import controlsFunction from './controls';
import realtimeCommands from './commands';
const logger = log4js.getLogger('default');