diff --git a/.eslintrc.json b/.eslintrc.json index 43b2fdd..454dec9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -37,6 +37,11 @@ // retropilot: we allow use of the continue statement "no-continue": "off", + // disallow else after a return in an if + // https://eslint.org/docs/rules/no-else-return + // retropilot: allow else-if... + "no-else-return": ["error", { "allowElseIf": true }], + // disallow use of variables before they are defined // http://eslint.org/docs/rules/no-use-before-define // retropilot: permit referencing functions before they're defined diff --git a/src/server/app.js b/src/server/app.js index 6b8d7e5..ed18aab 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -1,15 +1,13 @@ import cookieParser from 'cookie-parser'; import cors from 'cors'; import express from 'express'; -import rateLimit from 'express-rate-limit'; import log4js from 'log4js'; import storageController from './controllers/storage'; -import athena from './websocket/athena'; import controllers from './controllers'; -import routers from './router'; +import router from './router'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); function runAsyncWrapper(callback) { return function wrapper(req, res, next) { @@ -30,27 +28,7 @@ app.use(cors({ })); app.use(cookieParser()); -app.use('/api', routers.api); -app.use('/useradmin', routers.useradmin); - -if (process.env.ATHENA_ENABLED) { - const athenaRateLimit = rateLimit({ - windowMs: 30000, - max: process.env.ATHENA_API_RATE_LIMIT, - }); - - app.use((req, res, next) => { - req.athenaWebsocketTemp = athena; - return next(); - }); - - app.use('/admin', routers.admin); - app.use('/realtime', athenaRateLimit); - app.use('/realtime', routers.realtime); - // app.use(routers.oauthAuthenticator) -} else { - logger.info('Athena disabled'); -} +app.use(router); app.use('/favicon.ico', express.static('static/favicon.ico')); app.use(process.env.BASE_DRIVE_DOWNLOAD_PATH_MAPPING, express.static(process.env.STORAGE_PATH)); diff --git a/src/server/controllers/admin.js b/src/server/controllers/admin.js index fb434e1..44e27ce 100644 --- a/src/server/controllers/admin.js +++ b/src/server/controllers/admin.js @@ -5,7 +5,9 @@ import { Accounts } from '../../models'; async function isCurrentUserAdmin(hardFail, req) { const account = await authentication.getAuthenticatedAccount(req); - if (!account) return { isAdmin: false, account }; + if (!account) { + return { isAdmin: false, account }; + } if (account.admin !== 1) { return { isAdmin: false, account }; } diff --git a/src/server/controllers/authentication/index.js b/src/server/controllers/authentication/index.js index df274c5..f6f9720 100644 --- a/src/server/controllers/authentication/index.js +++ b/src/server/controllers/authentication/index.js @@ -4,7 +4,7 @@ import log4js from 'log4js'; import { Accounts } from '../../../models'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); export async function validateJWT(token, key) { try { @@ -43,22 +43,21 @@ async function signIn(email, password) { async function changePassword(account, newPassword, oldPassword) { if (!account || !newPassword || !oldPassword) { - return { success: false, error: 'MISSING_DATA' }; + return { success: false, code: 400, error: 'MISSING_DATA' }; } const oldPasswordHash = crypto.createHash('sha256').update(oldPassword + process.env.APP_SALT).digest('hex'); if (account.password !== oldPasswordHash) { - return { success: false, msg: 'BAD PASSWORD', passwordCorrect: false }; + return { success: false, code: 400, msg: 'BAD_PASSWORD' }; } const newPasswordHash = crypto.createHash('sha256').update(newPassword + process.env.APP_SALT).digest('hex'); - await Accounts.update( { password: newPasswordHash }, { where: { id: account.id } }, ); - return { success: true, msg: 'PASSWORD CHANGED', changed: true }; + return { success: true, msg: 'PASSWORD_CHANGED' }; } /* diff --git a/src/server/controllers/authentication/oauth/google.js b/src/server/controllers/authentication/oauth/google.js index 2efaed0..22e9d15 100644 --- a/src/server/controllers/authentication/oauth/google.js +++ b/src/server/controllers/authentication/oauth/google.js @@ -3,7 +3,7 @@ import { ClientCredentials, ResourceOwnerPassword, AuthorizationCode } from 'sim import log4js from 'log4js'; import { AUTH_OAUTH_ERR_GOOGLE, AUTH_OAUTH_ERR_GOOGLE_FAILED_TOKEN_FETCH } from '../../../consistency/terms'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); const keys = { web: { diff --git a/src/server/controllers/devices.js b/src/server/controllers/devices.js index dd38df7..3f5f6d8 100644 --- a/src/server/controllers/devices.js +++ b/src/server/controllers/devices.js @@ -13,7 +13,7 @@ import { import { readJWT, validateJWT } from './authentication'; import { getAccountFromId } from './users'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); const sanitize = sanitizeFactory(); async function pairDevice(account, qrString) { @@ -72,12 +72,13 @@ async function pairDeviceToAccountId(dongleId, accountId) { return { success: false, paired: false }; } -async function unpairDevice(account, dongleId) { +async function unpairDevice(dongleId, accountId) { const device = await Devices.getOne( - { where: { account_id: account.id, dongle_id: dongleId } }, + { where: { account_id: accountId, dongle_id: dongleId } }, ); if (device && device.dataValues) { + // TODO: check result? await Devices.update( { account_id: 0 }, { where: { dongle_id: dongleId } }, @@ -108,17 +109,24 @@ async function getDevices(accountId) { return Devices.findAll({ where: { account_id: accountId } }); } -async function getDeviceFromDongle(dongleId) { - if (!dongleId) return null; - const devices = await Devices.findOne({ where: { dongle_id: dongleId } }); - if (!devices || !devices.dataValues) { +async function getDeviceFromDongleId(dongleId) { + if (!dongleId) { return null; } - return devices.dataValues; + + const device = await Devices.findOne({ where: { dongle_id: dongleId } }); + if (!device || !device.dataValues) { + return null; + } + return device.dataValues; } + // TODO combine these redundant functions into one async function getDeviceFromSerial(serial) { - if (!serial) return null; + if (!serial) { + return null; + } + const devices = await Devices.findOne({ where: { serial } }); if (!devices || !devices.dataValues) { return null; @@ -127,8 +135,9 @@ async function getDeviceFromSerial(serial) { } async function updateDevice(dongleId, data) { - if (!dongleId) return null; - + if (!dongleId) { + return null; + } return Devices.update(data, { where: { dongle_id: dongleId } }); } @@ -163,7 +172,7 @@ async function isUserAuthorised(accountId, dongleId) { return { success: false, msg: 'bad_account', data: { authorised: false, account_id: accountId } }; } - const device = await getDeviceFromDongle(dongleId); + const device = await getDeviceFromDongleId(dongleId); if (!device) { return { success: false, msg: 'bad_device', data: { authorised: false, dongle_id: dongleId } }; } @@ -180,7 +189,7 @@ async function isUserAuthorised(accountId, dongleId) { } async function getOwnersFromDongle(dongleId) { - const device = await getDeviceFromDongle(dongleId); + const device = await getDeviceFromDongleId(dongleId); if (!device) { return { success: false }; } @@ -342,7 +351,7 @@ export default { unpairDevice, setDeviceNickname, getDevices, - getDeviceFromDongle, + getDeviceFromDongleId, setIgnoredUploads, getAllDevicesFiltered, pairDeviceToAccountId, diff --git a/src/server/controllers/index.js b/src/server/controllers/index.js index d491527..ebaa09c 100644 --- a/src/server/controllers/index.js +++ b/src/server/controllers/index.js @@ -1,4 +1,3 @@ -/* eslint-disable global-require, no-unused-vars */ import authentication from './authentication'; import helpers from './helpers'; import storage from './storage'; @@ -7,7 +6,6 @@ import users from './users'; import admin from './admin'; import devices from './devices'; -// TO DO, finish up removing this callback stuff export default { authentication, helpers, diff --git a/src/server/controllers/mailing.js b/src/server/controllers/mailing.js index 934fe7f..f5d8616 100644 --- a/src/server/controllers/mailing.js +++ b/src/server/controllers/mailing.js @@ -1,7 +1,7 @@ import nodemailer from 'nodemailer'; import log4js from 'log4js'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); const transporter = nodemailer.createTransport( { host: process.env.SMTP_HOST, diff --git a/src/server/controllers/storage.js b/src/server/controllers/storage.js index ed604d5..67bbce0 100644 --- a/src/server/controllers/storage.js +++ b/src/server/controllers/storage.js @@ -3,7 +3,7 @@ import fs from 'fs'; import log4js from 'log4js'; import path from 'path'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); let totalStorageUsed; diff --git a/src/server/controllers/users.js b/src/server/controllers/users.js index c8d0b53..0b18334 100644 --- a/src/server/controllers/users.js +++ b/src/server/controllers/users.js @@ -3,7 +3,7 @@ import log4js from 'log4js'; import { Accounts } from '../../models'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); export async function getAccountFromId(id) { return Accounts.findByPk(id); diff --git a/src/server/middlewares/authentication.js b/src/server/middlewares/authentication.js index 7c5b2b8..1e0f323 100644 --- a/src/server/middlewares/authentication.js +++ b/src/server/middlewares/authentication.js @@ -1,8 +1,12 @@ import authenticationController from '../controllers/authentication'; +export const getAccount = async (req, res, next) => { + req.account = await authenticationController.getAuthenticatedAccount(req); + next(); +}; + export const isAuthenticated = async (req, res, next) => { const account = await authenticationController.getAuthenticatedAccount(req); - if (!account) { res.status(401).json({ success: false, @@ -11,7 +15,6 @@ export const isAuthenticated = async (req, res, next) => { return; } - req.account = account; next(); }; diff --git a/src/server/router/administration/adminApi.js b/src/server/router/api/admin.js similarity index 97% rename from src/server/router/administration/adminApi.js rename to src/server/router/api/admin.js index 0a033e1..9119289 100644 --- a/src/server/router/administration/adminApi.js +++ b/src/server/router/api/admin.js @@ -53,7 +53,7 @@ router.get('/device/:dongle_id', runAsyncWrapper(async (req, res) => { return req.status(400).json({ error: true, msg: 'MISSING DATA', status: 400 }); } - const device = await controllers.devices.getDeviceFromDongle(dongleId); + const device = await controllers.devices.getDeviceFromDongleId(dongleId); return res.status(200).json({ success: true, data: device }); })); diff --git a/src/server/router/api/auth.js b/src/server/router/api/auth.js deleted file mode 100644 index 0f04b3c..0000000 --- a/src/server/router/api/auth.js +++ /dev/null @@ -1,50 +0,0 @@ -import bodyParser from 'body-parser'; -import express from 'express'; - -import authenticationController from '../../controllers/authentication'; -import { isAuthenticated } from '../../middlewares/authentication'; - -const router = express.Router(); - -router.get('/session', isAuthenticated, async (req, res) => { - return res.status(200).json({ - success: true, - data: { - user: req.account.dataValues, - }, - }); -}); - -router.post('/login', bodyParser.urlencoded({ extended: true }), async (req, res) => { - const signIn = await authenticationController.signIn(req.body.email, req.body.password); - if (!signIn.success) { - return res.status(401).json(signIn); - } - - const account = await authenticationController.getAccountFromJWT(signIn.jwt, true); - - return res.status(200).cookie('jwt', signIn.jwt).json({ - success: true, - data: { - jwt: signIn.jwt, - user: account.dataValues, - }, - }); -}); - -router.get('/logout', async (req, res) => { - res.clearCookie('session'); - return res.json({ success: true }); -}); - -// router.get('/session/get', async (req, res) => { -// const account = await authenticationController.getAuthenticatedAccount(req); -// -// if (!account) { -// res.json({ success: true, hasSession: false, session: {} }); -// } else { -// res.json({ success: true, hasSession: false, session: account }); -// } -// }); - -export default router; diff --git a/src/server/router/api/auth/index.js b/src/server/router/api/auth/index.js new file mode 100644 index 0000000..41a8c29 --- /dev/null +++ b/src/server/router/api/auth/index.js @@ -0,0 +1,84 @@ +import bodyParser from 'body-parser'; +import express from 'express'; + +import authenticationController from '../../../controllers/authentication'; +import { isAuthenticated } from '../../../middlewares/authentication'; +import { createAccount, verifyEmailToken } from '../../../controllers/users'; + +// /api/auth +const router = express.Router(); + +router.get('/session', isAuthenticated, async (req, res) => { + return res.status(200).json({ + success: true, + data: { + user: req.account.dataValues, + }, + }); +}); + +router.post('/login', bodyParser.urlencoded({ extended: true }), async (req, res) => { + const signIn = await authenticationController.signIn(req.body.email, req.body.password); + if (!signIn.success) { + return res.status(401).json(signIn); + } + + const account = await authenticationController.getAccountFromJWT(signIn.jwt); + + return res.status(200).cookie('jwt', signIn.jwt).json({ + success: true, + data: { + jwt: signIn.jwt, + user: account.dataValues, + }, + }); +}); + +router.post('/logout', async (req, res) => { + res.clearCookie('session'); + return res.json({ success: true }); +}); + +router.post('/register', bodyParser.urlencoded({ extended: true }), async (req, res) => { + const { email, password } = req.body; + if (!email || !password) { + // FIXME: use logger.warn + console.error('/useradmin/register/token - Malformed Request!'); + return res.status(400).json({ success: false, msg: 'malformed request' }); + } + + const accountStatus = await createAccount(req.body.email, req.body.password); + if (accountStatus && accountStatus.status) { + return res.status(accountStatus.status).json(accountStatus); + } + return res.status(500).json({ success: false, msg: 'contact server admin' }); +}); + +router.post('/register/verify', bodyParser.urlencoded({ extended: true }), async (req, res) => { + const { token } = req.body; + if (!token) { + return res.status(400).json({ + success: false, + data: { missingToken: true }, + }); + } + + const verified = await verifyEmailToken(req.params.token); + + if (verified && verified.status) { + return res.status(verified.status).json(verified); + } + return res.status(500).json({ success: false, msg: 'contact server admin' }); +}); + +// router.get('/session/get', async (req, res) => { +// const account = await authenticationController.getAuthenticatedAccount(req); +// +// if (!account) { +// res.json({ success: true, hasSession: false, session: {} }); +// } else { +// res.json({ success: true, hasSession: false, session: account }); +// } +// }); + +export default router; diff --git a/src/server/router/api/authentication/oauth.js b/src/server/router/api/auth/oauth.js similarity index 95% rename from src/server/router/api/authentication/oauth.js rename to src/server/router/api/auth/oauth.js index 7c820d1..97b6ceb 100644 --- a/src/server/router/api/authentication/oauth.js +++ b/src/server/router/api/auth/oauth.js @@ -5,7 +5,7 @@ import { getURL, getToken } from '../../../controllers/authentication/oauth/goog import { isAuthenticated } from '../../../middlewares/authentication'; const router = express.Router(); -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); router.get('/authentication/oauth/callback', async (req, res) => { logger.info(req.query); diff --git a/src/server/router/api/authentication/twofactor.js b/src/server/router/api/auth/twofactor.js similarity index 100% rename from src/server/router/api/authentication/twofactor.js rename to src/server/router/api/auth/twofactor.js diff --git a/src/server/router/api/devices.js b/src/server/router/api/devices.js index f9ee608..5d46fcc 100644 --- a/src/server/router/api/devices.js +++ b/src/server/router/api/devices.js @@ -8,7 +8,7 @@ import { isAuthenticated } from '../../middlewares/authentication'; import deviceController from '../../controllers/devices'; import { MutateDevice } from '../../schema/routes/devices'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); // /api/devices const router = express.Router(); diff --git a/src/server/router/api/index.js b/src/server/router/api/index.js index e6759f9..cef784c 100644 --- a/src/server/router/api/index.js +++ b/src/server/router/api/index.js @@ -1,14 +1,45 @@ import express from 'express'; +import rateLimit from 'express-rate-limit'; +import log4js from 'log4js'; +import athena from '../../websocket/athena'; + +import admin from './admin'; import auth from './auth'; import devices from './devices'; import realtime from './realtime'; +import useradmin from './useradmin'; -const router = express.Router(); +const logger = log4js.getLogger(); // /api +const router = express.Router(); + +router.use('/admin', admin); router.use('/auth', auth); router.use('/devices', devices); -router.use('/realtime', realtime); +router.use('/useradmin', useradmin); + +// TODO: setup oauth and twofactor endpoints +// app.use(routers.oauthAuthenticator); + +if (process.env.ATHENA_ENABLED) { + logger.info('Athena enabled'); + + const athenaRateLimit = rateLimit({ + windowMs: 30000, + max: process.env.ATHENA_API_RATE_LIMIT, + }); + + router.use((req, res, next) => { + req.athenaWebsocketTemp = athena; + return next(); + }); + + router.use('/realtime', athenaRateLimit); + router.use('/realtime', realtime); +} else { + logger.info('Athena disabled'); +} export default router; diff --git a/src/server/router/api/realtime.js b/src/server/router/api/realtime.js index 027ceea..057e55d 100644 --- a/src/server/router/api/realtime.js +++ b/src/server/router/api/realtime.js @@ -31,7 +31,7 @@ const whitelistParams = { router.get('/:dongleId/connected', isAuthenticated, async (req, res) => { const { account, params: { dongleId } } = req; - const device = await deviceController.getDeviceFromDongle(dongleId); + const device = await deviceController.getDeviceFromDongleId(dongleId); if (!device) { return res.status(400).json({ error: true, @@ -72,7 +72,7 @@ router.get('/:dongleId/send/:method/', isAuthenticated, async (req, res) => { }); } - const device = await deviceController.getDeviceFromDongle(dongleId); + const device = await deviceController.getDeviceFromDongleId(dongleId); if (!device) { return res.status(400).json({ error: true, @@ -110,7 +110,7 @@ router.get('/:dongle_id/get', async (req, res) => { errorObject: { authenticated: false }, }); } - const device = await deviceController.getDeviceFromDongle(req.params.dongle_id); + const device = await deviceController.getDeviceFromDongleId(req.params.dongle_id); if (!device) { return res.status(400).json({ error: true, @@ -147,7 +147,7 @@ router.get('/:dongle_id/temp/nav/:lat/:long', async (req, res) => { if (account == null) { return res.status(403).json({ error: true, errorMsg: 'Unauthenticated', errorObject: { authenticated: false } }); } - const device = await deviceController.getDeviceFromDongle(req.params.dongle_id); + const device = await deviceController.getDeviceFromDongleId(req.params.dongle_id); if (!device) { return res.status(400).json({ error: true, errorMsg: 'no_dongle', errorObject: { authenticated: true, dongle_exists: false } }); } diff --git a/src/server/router/api/registration.js b/src/server/router/api/registration.js deleted file mode 100644 index ee724d4..0000000 --- a/src/server/router/api/registration.js +++ /dev/null @@ -1,36 +0,0 @@ -import bodyParser from 'body-parser'; -import express from 'express'; - -import { createAccount, verifyEmailToken } from '../../controllers/users'; - -const router = express.Router(); -router.post('/retropilot/0/register/email', bodyParser.urlencoded({ extended: true }), async (req, res) => { - const { email, password } = req.body; - if (!email || !password) { - // FIXME: use logger.warn - console.error('/useradmin/register/token - Malformed Request!'); - return res.status(400).json({ success: false, msg: 'malformed request' }); - } - - const accountStatus = await createAccount(req.body.email, req.body.password); - if (accountStatus && accountStatus.status) { - return res.status(accountStatus.status).json(accountStatus); - } - return res.status(500).json({ success: false, msg: 'contact server admin' }); -}); - -router.get('/retropilot/0/register/verify/:token', bodyParser.urlencoded({ extended: true }), async (req, res) => { - const { token } = req.params; - if (!token) { - return res.status(400).json({ success: false, status: 400, data: { missingToken: true } }); - } - - const verified = await verifyEmailToken(req.params.token); - - if (verified && verified.status) { - return res.status(verified.status).json(verified); - } - return res.status(500).json({ success: false, msg: 'contact server admin' }); -}); - -export default router; diff --git a/src/server/router/api/useradmin.js b/src/server/router/api/useradmin.js new file mode 100644 index 0000000..96693ab --- /dev/null +++ b/src/server/router/api/useradmin.js @@ -0,0 +1,130 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import cookieParser from 'cookie-parser'; + +import controllers from '../../controllers'; +import deviceController from '../../controllers/devices'; +import { isAuthenticated } from '../../middlewares/authentication'; + +// TODO Remove this, pending on removing all auth logic from routes + +// /api/useradmin +const router = express.Router(); + +router.use(cookieParser()); + +function runAsyncWrapper(callback) { + return function wrapper(req, res, next) { + callback(req, res, next) + .catch(next); + }; +} + +let models; + +// FIXME: already provided in auth.js +router.post('/auth', bodyParser.urlencoded({ extended: true }), runAsyncWrapper(async (req, res) => { + const signIn = await controllers.authentication.signIn(req.body.email, req.body.password); + if (!signIn.success) { + return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid credentials or banned account')}`); + } + + return res.cookie('jwt', signIn.jwt).redirect('/useradmin/overview'); +})); + +// FIXME: already provided in auth.js +router.get('/signout', runAsyncWrapper(async (req, res) => { + res.clearCookie('session'); + return res.json({ success: true }); +})); + +router.get('/', runAsyncWrapper(async (req, res) => { + // TODO pull these values from db + const accounts = 0; + const devices = 0; + const drives = 0; + + return res.status(200).send({ + success: true, + data: { + serverStats: { + config: { + registerAllowed: process.env.ALLOW_REGISTRATION, + welcomeMessage: process.env.WELCOME_MESSAGE, + }, + accounts: accounts.num, + devices: devices.num, + drives: drives.num, + storageUsed: await controllers.storage.getTotalStorageUsed(), + }, + }, + }); +})); + +router.get('/overview', isAuthenticated, runAsyncWrapper(async (req, res) => { + const { account } = req; + const devices = await deviceController.getDevices(account.id); + + // TODO implement a _safe_ get account for these use cases to allow for data to be stripped prior to sending to the client. + delete (account.email_verify_token); + return res.status(200).json({ + success: true, + data: { + account, + devices, + }, + }); +})); + +router.get('/unpair_device/:dongleId', isAuthenticated, runAsyncWrapper(async (req, res) => { + const { account, params: { dongleId } } = req; + + const device = await deviceController.getDeviceFromDongleId(dongleId); + if (!device) { + return res.status(404).json({ success: false, msg: 'NOT_FOUND' }); + } else if (device.accountId !== account.id) { + return res.status(403).json({ success: false, msg: 'FORBIDDEN' }); + } + + const result = await deviceController.unpairDevice(dongleId, account.id); + if (!result.success) { + return res.status(500).json(result); + } + + return res.status(200).json({ success: true }); +})); + +router.post('/pair_device', [isAuthenticated, bodyParser.urlencoded({ extended: true })], runAsyncWrapper(async (req, res) => { + const { account, body: { qrString } } = req; + if (!qrString) { + return res.json({ success: false, msg: 'BAD_REQUEST', status: 400 }); + } + + const pairDevice = await controllers.devices.pairDevice(account, qrString); + if (!pairDevice.success) { + return res.json({ success: false, msg: 'error', data: pairDevice }); + } + + return res.json({ + success: true, + msg: 'Paired', + status: 200, + data: pairDevice, + }); +})); + +router.post('/password/change', [isAuthenticated, bodyParser.urlencoded({ extended: true })], runAsyncWrapper(async (req, res) => { + const { account, body: { oldPassword, newPassword } } = req; + const result = await controllers.authentication.changePassword( + account, + newPassword, + oldPassword, + ); + if (!result.success) { + return res.status(result.status).json(result); + } + + return res.json({ success: true }); +})); + +export default router; diff --git a/src/server/router/index.js b/src/server/router/index.js index c833ac1..fa80e42 100644 --- a/src/server/router/index.js +++ b/src/server/router/index.js @@ -1,21 +1,15 @@ -/* eslint-disable global-require */ +import express from 'express'; -import useradmin from './useradmin'; import api from './api'; -import useradminapi from './userAdminApi'; -import admin from './administration/adminApi'; -import realtime from './api/realtime'; -import deviceApi from './api/devices'; -import authenticationApi from './api/auth'; -import oauthAuthenticator from './api/authentication/oauth'; +import legacy from './legacy'; +import useradmin from './useradmin'; -export default { - useradmin, - api, - useradminapi, - admin, - realtime, - deviceApi, - authenticationApi, - oauthAuthenticator, -}; +const router = express.Router(); + +// TODO: refactor +router.use(legacy); + +router.use('/api', api); +router.use('/useradmin', useradmin); + +export default router; diff --git a/src/server/router/api.js b/src/server/router/legacy.js similarity index 79% rename from src/server/router/api.js rename to src/server/router/legacy.js index afdfe40..0172e14 100644 --- a/src/server/router/api.js +++ b/src/server/router/legacy.js @@ -1,17 +1,16 @@ -/* eslint-disable */ -import express from 'express'; import bodyParser from 'body-parser'; import crypto from 'crypto'; +import express from 'express'; import log4js from 'log4js'; -import storageController from '../controllers/storage'; + +import { validateJWT } from '../controllers/authentication'; import deviceController from '../controllers/devices'; -import authenticationController from '../controllers/authentication'; -import userController from '../controllers/users'; +import storageController from '../controllers/storage'; +import { getAccountFromId } from '../controllers/users'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); const router = express.Router(); - function runAsyncWrapper(callback) { return function wrapper(req, res, next) { callback(req, res, next) @@ -21,7 +20,6 @@ function runAsyncWrapper(callback) { // TODO(cameron): clean up this mess into separate files - // DRIVE & BOOT/CRASH LOG FILE UPLOAD HANDLING router.put('/backend/post_upload', bodyParser.raw({ inflate: true, @@ -67,7 +65,7 @@ router.get('/v1.1/devices/:dongleId/', runAsyncWrapper(async (req, res) => { const { dongleId } = req.params; logger.info(`HTTP.DEVICES called for ${req.params.dongleId}`); - const device = deviceController.getDeviceFromDongle(dongleId); + const device = deviceController.getDeviceFromDongleId(dongleId); if (!device) { logger.info(`HTTP.DEVICES device ${dongleId} not found`); @@ -75,7 +73,7 @@ router.get('/v1.1/devices/:dongleId/', runAsyncWrapper(async (req, res) => { } const decoded = device.public_key - ? await authenticationController.validateJWT(req.headers.authorization, device.public_key) + ? await validateJWT(req.headers.authorization, device.public_key) : null; if ((!decoded || decoded.identity !== req.params.dongleId)) { @@ -107,14 +105,14 @@ router.get('/v1.1/devices/:dongleId/stats', runAsyncWrapper(async (req, res) => }, }; - const device = await deviceController.getDeviceFromDongle(dongleId); + const device = await deviceController.getDeviceFromDongleId(dongleId); if (!device) { logger.info(`HTTP.STATS device ${dongleId} not found`); return res.status(404).json('Not found.'); } const decoded = device.public_key - ? await authenticationController.validateJWT(req.headers.authorization, device.public_key) + ? await validateJWT(req.headers.authorization, device.public_key) : null; if ((!decoded || decoded.identity !== req.params.dongleId)) { @@ -123,27 +121,26 @@ router.get('/v1.1/devices/:dongleId/stats', runAsyncWrapper(async (req, res) => } // TODO reimplement weekly stats - - /*const statresult = await models.get('SELECT COUNT(*) as routes, ROUND(SUM(distance_meters)/1609.34) as distance, ROUND(SUM(duration)/60) as duration FROM drives WHERE dongle_id=?', device.dongle_id); - if (statresult != null && statresult.routes != null) { - stats.all.routes = statresult.routes; - stats.all.distance = statresult.distance != null ? statresult.distance : 0; - stats.all.minutes = statresult.duration != null ? statresult.duration : 0; - } - - // this determines the date at 00:00:00 UTC of last monday (== beginning of the current "ISO"week) - const d = new Date(); - const day = d.getDay(); - const diff = d.getDate() - day + (day === 0 ? -6 : 1); - const lastMonday = new Date(d.setDate(diff)); - lastMonday.setHours(0, 0, 0, 0); - - const statresultweek = await models.get('SELECT COUNT(*) as routes, ROUND(SUM(distance_meters)/1609.34) as distance, ROUND(SUM(duration)/60) as duration FROM drives WHERE dongle_id=? AND drive_date >= ?', device.dongle_id, lastMonday.getTime()); - if (statresultweek != null && statresultweek.routes != null) { - stats.week.routes = statresultweek.routes; - stats.week.distance = statresultweek.distance != null ? statresultweek.distance : 0; - stats.week.minutes = statresultweek.duration != null ? statresultweek.duration : 0; - }*/ + // const statresult = await models.get('SELECT COUNT(*) as routes, ROUND(SUM(distance_meters)/1609.34) as distance, ROUND(SUM(duration)/60) as duration FROM drives WHERE dongle_id=?', device.dongle_id); + // if (statresult != null && statresult.routes != null) { + // stats.all.routes = statresult.routes; + // stats.all.distance = statresult.distance != null ? statresult.distance : 0; + // stats.all.minutes = statresult.duration != null ? statresult.duration : 0; + // } + // + // // this determines the date at 00:00:00 UTC of last monday (== beginning of the current "ISO"week) + // const d = new Date(); + // const day = d.getDay(); + // const diff = d.getDate() - day + (day === 0 ? -6 : 1); + // const lastMonday = new Date(d.setDate(diff)); + // lastMonday.setHours(0, 0, 0, 0); + // + // const statresultweek = await models.get('SELECT COUNT(*) as routes, ROUND(SUM(distance_meters)/1609.34) as distance, ROUND(SUM(duration)/60) as duration FROM drives WHERE dongle_id=? AND drive_date >= ?', device.dongle_id, lastMonday.getTime()); + // if (statresultweek != null && statresultweek.routes != null) { + // stats.week.routes = statresultweek.routes; + // stats.week.distance = statresultweek.distance != null ? statresultweek.distance : 0; + // stats.week.minutes = statresultweek.duration != null ? statresultweek.duration : 0; + // } logger.info(`HTTP.STATS for ${req.params.dongleId} returning: ${JSON.stringify(stats)}`); return res.status(200).json(stats); @@ -154,7 +151,7 @@ router.get('/v1/devices/:dongleId/owner', runAsyncWrapper(async (req, res) => { const { dongleId } = req.params; logger.info(`HTTP.OWNER called for ${req.params.dongleId}`); - const device = await deviceController.getDeviceFromDongle(dongleId); + const device = await deviceController.getDeviceFromDongleId(dongleId); if (!device) { logger.info(`HTTP.OWNER device ${dongleId} not found`); @@ -162,7 +159,7 @@ router.get('/v1/devices/:dongleId/owner', runAsyncWrapper(async (req, res) => { } const decoded = device.public_key - ? await authenticationController.validateJWT(req.headers.authorization, device.public_key) + ? await validateJWT(req.headers.authorization, device.public_key) : null; if ((!decoded || decoded.identity !== req.params.dongleId)) { @@ -171,17 +168,17 @@ router.get('/v1/devices/:dongleId/owner', runAsyncWrapper(async (req, res) => { } let owner = ''; - let points = 0; + const points = 0; - let account = await userController.getAccountFromId(device.account_id); + let account = await getAccountFromId(device.account_id); if (account != null && account.dataValues != null) { - account = account.dataValues + account = account.dataValues; [owner] = account.email.split('@'); // TODO reimplement "points" - /*const stats = await models.all('SELECT SUM(distance_meters) as points FROM drives WHERE dongle_id IN (SELECT dongle_id FROM devices WHERE account_id=?)', account.id); - if (stats != null && stats.points != null) { - points = stats.points; - }*/ + // const stats = await models.all('SELECT SUM(distance_meters) as points FROM drives WHERE dongle_id IN (SELECT dongle_id FROM devices WHERE account_id=?)', account.id); + // if (stats != null && stats.points != null) { + // points = stats.points; + // } } const response = { username: owner, points }; @@ -196,14 +193,14 @@ async function upload(req, res) { const auth = req.headers.authorization; logger.info(`HTTP.UPLOAD_URL called for ${req.params.dongleId} and file ${path}: ${JSON.stringify(req.headers)}`); - const device = await deviceController.getDeviceFromDongle(dongleId); + const device = await deviceController.getDeviceFromDongleId(dongleId); if (!device) { logger.info(`HTTP.UPLOAD_URL device ${dongleId} not found or not linked to an account / refusing uploads`); return res.send('Unauthorized.').status(400); } const decoded = device.public_key - ? await authenticationController.validateJWT(req.headers.authorization, device.public_key).catch(logger.error) + ? await validateJWT(req.headers.authorization, device.public_key).catch(logger.error) : null; if ((!decoded || decoded.identity !== req.params.dongleId)) { @@ -268,15 +265,16 @@ async function upload(req, res) { responseUrl = `${process.env.BASE_UPLOAD_URL}?file=${filename}&dir=${directory}&dongleId=${dongleId}&ts=${ts}&token=${token}`; logger.info(`HTTP.UPLOAD_URL matched 'drive' file upload, constructed responseUrl: ${responseUrl}`); - const drive = await deviceController.getDriveFromIdentifier(dongleId, driveName).catch((err)=>{ - logger.warn("drive failed to make", err) - }) + const drive = await deviceController.getDriveFromIdentifier(dongleId, driveName) + .catch((err) => { + logger.warn('drive failed to make', err); + }); - logger.info("drive value", drive) - logger.info("drive name:", driveName) + logger.info('drive value', drive); + logger.info('drive name:', driveName); if (drive === undefined || drive === null) { - logger.info("CREATING NEW DRIVE") + logger.info('CREATING NEW DRIVE'); // create a new drive const timeSplit = driveName.split('--'); const timeString = `${timeSplit[0]} ${timeSplit[1].replace(/-/g, ':')}`; @@ -294,7 +292,7 @@ async function upload(req, res) { is_preserved: false, is_deleted: false, is_physically_removed: false, - }) + }); await deviceController.updateOrCreateDriveSegment(dongleId, driveName, segment, { duration: 0, @@ -303,27 +301,26 @@ async function upload(req, res) { is_processed: false, is_stalled: false, created: Date.now(), - }) + }); logger.info(`HTTP.UPLOAD_URL created new drive #${JSON.stringify(driveResult.lastID)}`); } else { - logger.info("UPDATING DRIVE") + logger.info('UPDATING DRIVE'); await deviceController.updateOrCreateDrive(dongleId, driveName, { max_segment: Math.max(drive.max_segment, segment), upload_complete: false, is_processed: false, last_upload: Date.now(), - }) - - await deviceController.updateOrCreateDriveSegment(dongleId, driveName, segment, { - duration: 0, - distance_meters: 0, - upload_complete: false, - is_processed: false, - is_stalled: false, - created: Date.now() - }) + }); + await deviceController.updateOrCreateDriveSegment(dongleId, driveName, segment, { + duration: 0, + distance_meters: 0, + upload_complete: false, + is_processed: false, + is_stalled: false, + created: Date.now(), + }); logger.info(`HTTP.UPLOAD_URL updated existing drive: ${JSON.stringify(drive)}`); } @@ -347,7 +344,7 @@ router.post('/v2/pilotauth/', bodyParser.urlencoded({ extended: true }), async ( const { serial, public_key: publicKey, - register_token: registerToken + register_token: registerToken, } = req.query; if ( @@ -359,13 +356,13 @@ router.post('/v2/pilotauth/', bodyParser.urlencoded({ extended: true }), async ( return res.status(400).send('Malformed Request.'); } - const decoded = await authenticationController.validateJWT(registerToken, publicKey); + const decoded = await validateJWT(registerToken, publicKey); if (!decoded || !decoded.register) { logger.error(`HTTP.V2.PILOTAUTH JWT token is invalid (${JSON.stringify(decoded)})`); return res.status(400).send('Malformed Request.'); } - const device = await deviceController.getDeviceFromSerial(serial) + const device = await deviceController.getDeviceFromSerial(serial); if (device == null) { logger.info(`HTTP.V2.PILOTAUTH REGISTERING NEW DEVICE (${imei1}, ${serial})`); @@ -373,23 +370,31 @@ router.post('/v2/pilotauth/', bodyParser.urlencoded({ extended: true }), async ( // eslint-disable-next-line no-constant-condition while (true) { const dongleId = crypto.randomBytes(4).toString('hex'); - const isDongleIdTaken = await deviceController.getDeviceFromDongle(dongleId); + const isDongleIdTaken = await deviceController.getDeviceFromDongleId(dongleId); if (isDongleIdTaken == null) { - await deviceController.createDongle(dongleId, 0, imei1, serial, publicKey) + await deviceController.createDongle(dongleId, 0, imei1, serial, publicKey); - - const newDevice = await deviceController.getDeviceFromDongle(dongleId); + const newDevice = await deviceController.getDeviceFromDongleId(dongleId); logger.info(`HTTP.V2.PILOTAUTH REGISTERED NEW DEVICE: ${JSON.stringify(newDevice)}`); - return res.status(200).json({ dongle_id: newDevice.dongle_id, access_token: 'DEPRECATED-BUT-REQUIRED-FOR-07' }); + return res.status(200).json({ + dongle_id: newDevice.dongle_id, + access_token: 'DEPRECATED-BUT-REQUIRED-FOR-07', + }); } } } - await deviceController.updateDevice(device.dongle_id, {last_ping: Date.now(), public_key: publicKey}) + await deviceController.updateDevice(device.dongle_id, { + last_ping: Date.now(), + public_key: publicKey, + }); logger.info(`HTTP.V2.PILOTAUTH REACTIVATING KNOWN DEVICE (${imei1}, ${serial}) with dongle_id ${device.dongle_id}`); - return res.status(200).json({ dongle_id: device.dongle_id, access_token: 'DEPRECATED-BUT-REQUIRED-FOR-07' }); + return res.status(200).json({ + dongle_id: device.dongle_id, + access_token: 'DEPRECATED-BUT-REQUIRED-FOR-07', + }); }); // RETRIEVES DATASET FOR OUR MODIFIED CABANA - THIS RESPONSE IS USED TO FAKE A DEMO ROUTE @@ -400,7 +405,7 @@ router.get('/useradmin/cabana_drive/:extendedRouteIdentifier', runAsyncWrapper(a const driveIdentifier = params[2]; const driveIdentifierHashReq = params[3]; - const drive = await deviceController.getDrive(dongleId, driveIdentifier) + const drive = await deviceController.getDrive(dongleId, driveIdentifier); if (!drive) { return res.status(200).json({ status: 'drive not found' }); diff --git a/src/server/router/userAdminApi.js b/src/server/router/userAdminApi.js deleted file mode 100644 index e3e9d31..0000000 --- a/src/server/router/userAdminApi.js +++ /dev/null @@ -1,508 +0,0 @@ -/* eslint-disable max-len */ -import express from 'express'; -import bodyParser from 'body-parser'; -import cookieParser from 'cookie-parser'; -import controllers from '../controllers'; -import deviceController from '../controllers/devices'; - -// TODO Remove this, pending on removing all auth logic from routes -const router = express.Router(); -router.use(cookieParser()); - -function runAsyncWrapper(callback) { - return function wrapper(req, res, next) { - callback(req, res, next) - .catch(next); - }; -} - -/* eslint-disable no-unused-vars */ -let models; -let logger; -/* eslint-enable no-unused-vars */ - -router.post('/retropilot/0/useradmin/auth', bodyParser.urlencoded({ extended: true }), runAsyncWrapper(async (req, res) => { - const signIn = await controllers.authentication.signIn(req.body.email, req.body.password); - if (!signIn.success) { - return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid credentials or banned account')}`); - } - - return res.cookie('jwt', signIn.jwt).redirect('/useradmin/overview'); -})); - -router.get('/retropilot/0/useradmin/signout', runAsyncWrapper(async (req, res) => { - res.clearCookie('session'); - return res.json({ success: true }); -})); - -router.get('/retropilot/0/useradmin', runAsyncWrapper(async (req, res) => { - // TODO pull these values from db - const accounts = 0; - const devices = 0; - const drives = 0; - - return res.status(200).send({ - success: true, - data: { - serverStats: { - config: { - registerAllowed: process.env.ALLOW_REGISTRATION, - welcomeMessage: process.env.WELCOME_MESSAGE, - }, - accounts: accounts.num, - devices: devices.num, - drives: drives.num, - storageUsed: await controllers.storage.getTotalStorageUsed(), - }, - }, - }); -})); - -/* - Requires username and password to register - */ - -/* -router.post('/useradmin/register/token', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { - const email = req.body.email; - - if (!process.env.ALLOW_REGISTRATION) { - res.send('Unauthorized.').status(401); - return; - } - - const authAccount = await controllers.authentication.getAuthenticatedAccount(req); - if (authAccount != null) { - res.redirect('/useradmin/overview'); - return; - } - - const account = await models.__db.get('SELECT * FROM accounts WHERE LOWER(email) = ?', email.trim().toLowerCase()); - if (account != null) { - res.redirect('/useradmin/register?status=' + encodeURIComponent('Email is already registered')); - return; - } - - const token = crypto.createHmac('sha256', process.env.APP_SALT).update(email.trim()).digest('hex'); - - let infoText = ''; - - if (req.body.token === undefined) { // email entered, token request - infoText = 'Please check your inbox (SPAM) for an email with the registration token.
If the token was not delivered, please ask the administrator to check the server.log for the token generated for your email.

'; - - const emailStatus = await controllers.mailing.sendEmailVerification(token, email); - - } else { // final registration form filled - if (req.body.token != token) { - infoText = 'The registration token you entered was incorrect, please try again.

'; - } else if (req.body.password != req.body.password2 || req.body.password.length < 3) { - infoText = 'The passwords you entered did not or were shorter than 3 characters, please try again.

'; - } else { - - const result = await models.__db.run( - 'INSERT INTO accounts (email, password, created, banned) VALUES (?, ?, ?, ?)', - email, - crypto.createHash('sha256').update(req.body.password + process.env.APP_SALT).digest('hex'), - Date.now(), false); - - if (result.lastID != undefined) { - logger.info("USERADMIN REGISTRATION - created new account #" + result.lastID + " with email " + email + ""); - res.cookie('session', { - account: email, - expires: Date.now() + 1000 * 3600 * 24 * 365 - }, {signed: true}); - res.redirect('/useradmin/overview'); - return; - } else { - logger.error("USERADMIN REGISTRATION - account creation failed, resulting account data for email " + email + " is: " + result); - infoText = 'Unable to complete account registration (database error).

'; - } - } - } - - res.status(200); - res.send('

Welcome To The RetroPilot Server Dashboard!

' + - ` - < < < Back To Login -

-

Register / Finish Registration

- ` + infoText + ` -
- -
- - - - `); -})) - -router.get('/useradmin/register', runAsyncWrapper(async (req, res) => { - if (!process.env.ALLOW_REGISTRATION) { - res.status(400); - res.send('Unauthorized.'); - return; - } - - const account = await controllers.authentication.getAuthenticatedAccount(req); - if (account != null) { - res.redirect('/useradmin/overview'); - return; - } - - res.status(200); - res.send('

Welcome To The RetroPilot Server Dashboard!

' + - ` - < < < Back To Login -

-

Register / Request Email Token

- ` + (req.query.status !== undefined ? '' + htmlspecialchars(req.query.status) + '
' : '') + ` - - - - `); -})) -*/ - -router.get('/retropilot/0/overview', runAsyncWrapper(async (req, res) => { - const account = await controllers.authentication.getAuthenticatedAccount(req); - if (account == null) { - res.send({ success: false, data: { session: false } }); - - return; - } - - const devices = await models.__db.all('SELECT * FROM devices WHERE account_id = ? ORDER BY dongle_id ASC', account.id); - - // TODO implement a _safe_ get account for these use cases to allow for data to be stripped prior to sending to the client. - delete (account.email_verify_token); - res.json({ - success: true, - data: { - account, - devices, - }, - - }).status(200); -})); - -router.get('/retropilot/0/unpair_device/:dongleId', runAsyncWrapper(async (req, res) => { - const account = await controllers.authentication.getAuthenticatedAccount(req); - if (account == null) { - return res.json({ success: false, data: { session: false } }).status(403); - } - const device = await models.__db.get('SELECT * FROM devices WHERE account_id = ? AND dongle_id = ?', account.id, req.params.dongleId); - - if (device == null) { - return res.json({ success: false }).status(400); - } - - const pairDeviceToAccountId = await deviceController.pairDeviceToAccountId(req.prams.dongleId, 0); - - if (pairDeviceToAccountId.success && pairDeviceToAccountId.paired) { - return res.json({ success: true, data: { unlink: true } }); - } - return res.json({ success: true, data: { unlink: false } }); -})); - -router.post('/retropilot/0/pair_device', bodyParser.urlencoded({ extended: true }), runAsyncWrapper(async (req, res) => { - const account = await controllers.authentication.getAuthenticatedAccount(req); - if (account == null) { - return res.json({ success: false, msg: 'UNAUTHORISED', status: 403 }); - } - - const { qr_string: qrString } = req.body; - if (!qrString) { - return res.json({ success: false, msg: 'BAD_REQUEST', status: 400 }); - } - - const pairDevice = await controllers.devices.pairDevice(account, qrString); - if (!pairDevice.success) { - return res.json({ success: false, msg: 'error', data: pairDevice }); - } - - return res.json({ - success: true, - msg: 'Paired', - status: 200, - data: pairDevice, - }); -})); - -router.post('/retropilot/0/password/change', bodyParser.urlencoded({ extended: true }), runAsyncWrapper(async (req, res) => { - const account = await controllers.authentication.getAuthenticatedAccount(req); - if (account == null) { - return res.json({ success: false, msg: 'UNAUTHORISED', status: 403 }); - } - - const pwChange = await controllers.authentication.changePassword(account, req.body.newPassword, req.body.oldPassword); - if (!pwChange.success) { - return res.json({ success: false, data: pwChange }); - } - - return res.json({ success: true }); -})); - -/* - -router.get('/useradmin/device/:dongleId', runAsyncWrapper(async (req, res) => { - const account = await controllers.authentication.getAuthenticatedAccount(req); - if (account == null) { - res.redirect('/useradmin?status=' + encodeURIComponent('Invalid or expired session')); - return; - } - - const device = await models.__db.get('SELECT * FROM devices WHERE account_id = ? AND dongle_id = ?', account.id, req.params.dongleId); - - if (device == null) { - res.status(400); - res.send('Unauthorized.'); - return; - } - - const drives = await models.__db.all('SELECT * FROM drives WHERE dongle_id = ? AND is_deleted = ? ORDER BY created DESC', device.dongle_id, false); - - var dongleIdHash = crypto.createHmac('sha256', process.env.APP_SALT).update(device.dongle_id).digest('hex'); - - const bootlogDirectoryTree = dirTree(process.env.STORAGE_PATH + device.dongle_id + "/" + dongleIdHash + "/boot/", {attributes: ['size']}); - var bootlogFiles = []; - if (bootlogDirectoryTree != undefined) { - for (var i = 0; i < bootlogDirectoryTree.children.length; i++) { - - var timeSplit = bootlogDirectoryTree.children[i].name.replace('boot-', '').replace('crash-', '').replace('\.bz2', '').split('--'); - var timeString = timeSplit[0] + ' ' + timeSplit[1].replace(/-/g, ':'); - bootlogFiles.push({ - 'name': bootlogDirectoryTree.children[i].name, - 'size': bootlogDirectoryTree.children[i].size, - 'date': Date.parse(timeString) - }); - } - bootlogFiles.sort((a, b) => (a.date < b.date) ? 1 : -1); - } - - const crashlogDirectoryTree = dirTree(process.env.STORAGE_PATH + device.dongle_id + "/" + dongleIdHash + "/crash/", {attributes: ['size']}); - var crashlogFiles = []; - if (crashlogDirectoryTree != undefined) { - for (var i = 0; i < crashlogDirectoryTree.children.length; i++) { - - var timeSplit = crashlogDirectoryTree.children[i].name.replace('boot-', '').replace('crash-', '').replace('\.bz2', '').split('--'); - var timeString = timeSplit[0] + ' ' + timeSplit[1].replace(/-/g, ':'); - crashlogFiles.push({ - 'name': crashlogDirectoryTree.children[i].name, - 'size': crashlogDirectoryTree.children[i].size, - 'date': Date.parse(timeString) - }); - } - crashlogFiles.sort((a, b) => (a.date < b.date) ? 1 : -1); - } - - var response = '

Welcome To The RetroPilot Server Dashboard!

' + - - ` - < < < Back To Overview -

Device ` + device.dongle_id + `

- Type: ` + device.device_type + `
- Serial: ` + device.serial + `
- IMEI: ` + device.imei + `
- Registered: ` + controllers.helpers.formatDate(device.created) + `
- Last Ping: ` + controllers.helpers.formatDate(device.last_ping) + `
- Public Key:
` + device.public_key.replace(/\r?\n|\r/g, "
") + `
-
- Stored Drives: ` + drives.length + `
- Quota Storage: ` + device.storage_used + ` MB / ` + process.env.DEVICE_STORAGE_QUOTA_MB + ` MB
-
- `; - - response += `Boot Logs (last 5):
- - - `; - for (var i = 0; i < Math.min(5, bootlogFiles.length); i++) { - response += ``; - } - response += `
datefilesize
` + controllers.helpers.formatDate(bootlogFiles[i].date) + `` + bootlogFiles[i].name + `` + bootlogFiles[i].size + `


`; - - response += `Crash Logs (last 5):
- - - `; - for (var i = 0; i < Math.min(5, crashlogFiles.length); i++) { - response += ``; - } - response += `
datefilesize
` + controllers.helpers.formatDate(crashlogFiles[i].date) + `` + crashlogFiles[i].name + `` + crashlogFiles[i].size + `


`; - - response += `Drives (non-preserved drives expire ` + process.env.DEVICE_EXPIRATION_DAYS + ` days after upload):
- - - `; - - for (var i in drives) { - response += ''; - } - response += `
identifierfilesizedurationdistance_metersupload_completeis_processedupload_dateactions
' + (drives[i].is_preserved ? '' : '') + drives[i].identifier + (drives[i].is_preserved ? '' : '') + '' + Math.round(drives[i].filesize / 1024) + ' MiB' + controllers.helpers.formatDuration(drives[i].duration) + '' + Math.round(drives[i].distance_meters / 1000) + ' km' + drives[i].upload_complete + '' + drives[i].is_processed + '' + controllers.helpers.formatDate(drives[i].created) + '' + '[delete]' + (drives[i].is_preserved ? '' : '  [preserve]') + '
-
-
- Unpair Device -

-
- Sign Out`; - - res.status(200); - res.send(response); - -})) - -router.get('/useradmin/drive/:dongleId/:driveIdentifier/:action', runAsyncWrapper(async (req, res) => { - const account = await controllers.authentication.getAuthenticatedAccount(req); - if (account == null) { - res.redirect('/useradmin?status=' + encodeURIComponent('Invalid or expired session')); - return; - } - - const device = await models.__db.get('SELECT * FROM devices WHERE account_id = ? AND dongle_id = ?', account.id, req.params.dongleId); - - if (device == null) { - res.status(400); - res.send('Unauthorized.'); - return; - } - - const drive = await models.__db.get('SELECT * FROM drives WHERE identifier = ? AND dongle_id = ?', req.params.driveIdentifier, req.params.dongleId); - - if (drive == null) { - res.status(400); - res.send('Unauthorized.'); - return; - } - - if (req.params.action == 'delete') { - const result = await models.__db.run( - 'UPDATE drives SET is_deleted = ? WHERE id = ?', - true, drive.id - ); - } else if (req.params.action == 'preserve') { - const result = await models.__db.run( - 'UPDATE drives SET is_preserved = ? WHERE id = ?', - true, drive.id - ); - } - - res.redirect('/useradmin/device/' + device.dongle_id); - -})) - -router.get('/useradmin/drive/:dongleId/:driveIdentifier', runAsyncWrapper(async (req, res) => { - const account = await controllers.authentication.getAuthenticatedAccount(req); - - if (account == null) { - res.redirect('/useradmin?status=' + encodeURIComponent('Invalid or expired session')); - return; - } - - const device = await models.__db.get('SELECT * FROM devices WHERE account_id = ? AND dongle_id = ?', account.id, req.params.dongleId); - - if (device == null) { - res.status(400); - res.send('Unauthorized.'); - return; - } - - const drive = await models.__db.get('SELECT * FROM drives WHERE identifier = ? AND dongle_id = ?', req.params.driveIdentifier, req.params.dongleId); - - if (drive == null) { - res.status(400); - res.send('Unauthorized.'); - return; - } - - var dongleIdHash = crypto.createHmac('sha256', process.env.APP_SALT).update(device.dongle_id).digest('hex'); - var driveIdentifierHash = crypto.createHmac('sha256', process.env.APP_SALT).update(drive.identifier).digest('hex'); - - var driveUrl = process.env.BASE_DRIVE_DOWNLOAD_URL + device.dongle_id + "/" + dongleIdHash + "/" + driveIdentifierHash + "/" + drive.identifier + "/"; - - var cabanaUrl = null; - if (drive.is_processed) { - cabanaUrl = process.env.CABANA_URL + '?retropilotIdentifier=' + device.dongle_id + '|' + dongleIdHash + '|' + drive.identifier + '|' + driveIdentifierHash + '&retropilotHost=' + encodeURIComponent(process.env.BASE_URL) + '&demo=1"'; - } - - const directoryTree = dirTree(process.env.STORAGE_PATH + device.dongle_id + "/" + dongleIdHash + "/" + driveIdentifierHash + "/" + drive.identifier); - - var response = '

Welcome To The RetroPilot Server Dashboard!

' + - ` - < < < Back To Device ` + device.dongle_id + ` -

Drive ` + drive.identifier + ` on ` + drive.dongle_id + `

- Drive Date: ` + controllers.helpers.formatDate(drive.drive_date) + `
- Upload Date: ` + controllers.helpers.formatDate(drive.created) + `
- Num Segments: ` + (drive.max_segment + 1) + `
- Storage: ` + Math.round(drive.filesize / 1024) + ` MiB
- Duration: ` + controllers.helpers.formatDuration(drive.duration) + `
- Distance: ` + Math.round(drive.distance_meters / 1000) + ` km
- Is Preserved: ` + drive.is_preserved + `
- Upload Complete: ` + drive.upload_complete + `
- Processed: ` + drive.is_processed + `
-

- ` + (cabanaUrl ? 'View Drive in CABANA

' : '') + ` - Files:
- - - `; - - var directorySegments = {}; - for (var i in directoryTree.children) { - // skip any non-directory entries (for example m3u8 file in the drive directory) - if (directoryTree.children[i].type !== 'directory') continue; - - var segment = directoryTree.children[i].name; - - var qcamera = '--'; - var fcamera = '--'; - var dcamera = '--'; - var qlog = '--'; - var rlog = '--'; - for (var c in directoryTree.children[i].children) { - if (directoryTree.children[i].children[c].name == 'fcamera.hevc') fcamera = '' + directoryTree.children[i].children[c].name + ''; - if (directoryTree.children[i].children[c].name == 'dcamera.hevc') fcamera = '' + directoryTree.children[i].children[c].name + ''; - if (directoryTree.children[i].children[c].name == 'qcamera.ts') qcamera = '' + directoryTree.children[i].children[c].name + ''; - if (directoryTree.children[i].children[c].name == 'qlog.bz2') qlog = '' + directoryTree.children[i].children[c].name + ''; - if (directoryTree.children[i].children[c].name == 'rlog.bz2') rlog = '' + directoryTree.children[i].children[c].name + ''; - } - - var isProcessed = '?'; - var isStalled = '?'; - - const drive_segment = await models.__db.get('SELECT * FROM drive_segments WHERE segment_id = ? AND drive_identifier = ? AND dongle_id = ?', parseInt(segment), drive.identifier, device.dongle_id); - - if (drive_segment) { - isProcessed = drive_segment.is_processed; - isStalled = drive_segment.is_stalled; - } - - directorySegments["seg-" + segment] = ''; - } - - var qcamera = '--'; - var fcamera = '--'; - var dcamera = '--'; - var qlog = '--'; - var rlog = '--'; - var isProcessed = '?'; - var isStalled = '?'; - - for (var i = 0; i <= drive.max_segment; i++) { - if (directorySegments["seg-" + i] == undefined) { - response += ''; - } else - response += directorySegments["seg-" + i]; - } - - response += `
segmentqcameraqlogfcamerarlogdcameraprocessedstalled
' + segment + '' + qcamera + '' + qlog + '' + fcamera + '' + rlog + '' + dcamera + '' + isProcessed + '' + isStalled + '
' + i + '' + qcamera + '' + qlog + '' + fcamera + '' + rlog + '' + dcamera + '' + isProcessed + '' + isStalled + '
-

-
- Sign Out`; - - res.status(200); - res.send(response); - -})) -*/ - -export default router; diff --git a/src/server/router/useradmin.js b/src/server/router/useradmin.js index b068197..e2b3652 100644 --- a/src/server/router/useradmin.js +++ b/src/server/router/useradmin.js @@ -12,8 +12,9 @@ import helperController from '../controllers/helpers'; import mailingController from '../controllers/mailing'; import deviceController from '../controllers/devices'; import userController from '../controllers/users'; +import { getAccount, isAuthenticated } from '../middlewares/authentication'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); let models; const router = express.Router(); // TODO Remove this, pending on removing all auth logic from routes @@ -228,8 +229,8 @@ router.get('/useradmin/overview', runAsyncWrapper(async (req, res) => {

Pair New Devices

* To pair a new device, first have it auto-register on this server.
Then scan the QR Code and paste the Device Token below.

${req.query.linkstatus !== undefined ? `
${htmlspecialchars(req.query.linkstatus)}

` : ''} - - + +


@@ -241,7 +242,7 @@ ${req.query.linkstatus !== undefined ? `
${htmlspecialchars(req.query.link return res.status(200).send(response); })); -router.get('/useradmin/unpair_device/:dongleId', runAsyncWrapper(async (req, res) => { +router.get('/api/useradmin/unpair_device/:dongleId', runAsyncWrapper(async (req, res) => { const account = await authenticationController.getAuthenticatedAccount(req); if (account == null) { return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid or expired session')}`); @@ -250,14 +251,14 @@ router.get('/useradmin/unpair_device/:dongleId', runAsyncWrapper(async (req, res return res.redirect('/useradmin/overview'); })); -router.post('/useradmin/pair_device', bodyParser.urlencoded({ extended: true }), runAsyncWrapper(async (req, res) => { - const account = await authenticationController.getAuthenticatedAccount(req); - if (account == null) { +router.post('/useradmin/pair_device', [getAccount, bodyParser.urlencoded({ extended: true })], runAsyncWrapper(async (req, res) => { + const { account, body: { qrString } } = req; + if (!account) { res.redirect(`/useradmin?status=${encodeURIComponent('Invalid or expired session')}`); return; } - const pairDevice = await deviceController.pairDevice(account, req.body.qr_string); + const pairDevice = await deviceController.pairDevice(account, req.body.qrString); if (pairDevice.success === true) { res.redirect('/useradmin/overview'); } else if (pairDevice.registered === true) { @@ -281,7 +282,7 @@ router.get('/useradmin/device/:dongleId', runAsyncWrapper(async (req, res) => { return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid or expired session')}`); } - const device = await deviceController.getDeviceFromDongle(req.params.dongleId); + const device = await deviceController.getDeviceFromDongleId(req.params.dongleId); if (device == null || device.account_id !== account.id) { return res.status(400).send('Unauthorized.'); } @@ -428,7 +429,7 @@ router.get('/useradmin/drive/:dongleId/:driveIdentifier', runAsyncWrapper(async return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid or expired session')}`); } - const device = await deviceController.getDeviceFromDongle(req.params.dongleId); + const device = await deviceController.getDeviceFromDongleId(req.params.dongleId); if (device == null || device.account_id !== account.id) { return res.status(400).send('Unauthorized.'); } diff --git a/src/server/server.js b/src/server/server.js index fc05138..24712ca 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -5,7 +5,7 @@ import log4js from 'log4js'; import app from './app'; app.then((server) => { - const logger = log4js.getLogger('default'); + const logger = log4js.getLogger(); const httpServer = http.createServer(server); httpServer.listen(process.env.HTTP_PORT, () => { diff --git a/src/server/websocket/athena/index.js b/src/server/websocket/athena/index.js index 6a8edd9..b039d43 100644 --- a/src/server/websocket/athena/index.js +++ b/src/server/websocket/athena/index.js @@ -10,7 +10,7 @@ import { AthenaActionLog, AthenaReturnedData } from '../../../models'; import deviceController from '../../controllers/devices'; import helperFunctions from './helpers'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); let helpers; let wss; @@ -78,7 +78,7 @@ async function manageConnection(ws, res) { ws.on('message', async (message) => { heartbeat.call(ws); if (!ws.dongleId) { - wss.retropilotFunc.actionLogger(null, null, 'ATHENA_DEVICE_UNATHENTICATED_MESSAGE', null, ws._socket.remoteAddress, JSON.stringify([message]), ws.dongleId); + wss.retropilotFunc.actionLogger(null, null, 'ATHENA_DEVICE_UNAUTHENTICATED_MESSAGE', null, ws._socket.remoteAddress, JSON.stringify([message]), ws.dongleId); console.log('unauthenticated message, discarded'); return; } @@ -133,7 +133,7 @@ wss.retropilotFunc = { return false; } - const device = await deviceController.getDeviceFromDongle(unsafeJwt.identity); + const device = await deviceController.getDeviceFromDongleId(unsafeJwt.identity); let verifiedJWT; console.log('JWT', cookies.jwt); diff --git a/src/server/websocket/web/index.js b/src/server/websocket/web/index.js index 221443f..0d17782 100644 --- a/src/server/websocket/web/index.js +++ b/src/server/websocket/web/index.js @@ -8,7 +8,7 @@ import athenaRealtime from '../athena'; import controlsFunction from './controls'; import realtimeCommands from './commands'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); let server; let wss; diff --git a/src/worker/index.js b/src/worker/index.js index 27149ab..1b262ca 100644 --- a/src/worker/index.js +++ b/src/worker/index.js @@ -11,7 +11,7 @@ process.on('unhandledRejection', (error, p) => { console.dir(error.stack); }); -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); // make sure bunzip2 is available try { diff --git a/src/worker/storage.js b/src/worker/storage.js index 902f624..aa15077 100644 --- a/src/worker/storage.js +++ b/src/worker/storage.js @@ -2,7 +2,7 @@ import path from 'path'; import fs from 'fs'; import log4js from 'log4js'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); export function initializeStorage() { const verifiedPath = mkDirByPathSync(process.env.STORAGE_PATH, { isRelativeToScript: (process.env.STORAGE_PATH.indexOf('/') !== 0) }); diff --git a/src/worker/worker.js b/src/worker/worker.js index 3d2f60b..fa53426 100644 --- a/src/worker/worker.js +++ b/src/worker/worker.js @@ -14,7 +14,7 @@ import { deleteFolderRecursive, } from './storage'; -const logger = log4js.getLogger('default'); +const logger = log4js.getLogger(); const startTime = Date.now(); let lastCleaningTime = 0; diff --git a/test/routes/userAdminApi.test.js b/test/routes/userAdminApi.test.js index d169d56..249b58a 100644 --- a/test/routes/userAdminApi.test.js +++ b/test/routes/userAdminApi.test.js @@ -10,7 +10,7 @@ export default (app) => { describe('/api', () => { it('Load general app stats', (done) => { request(server) - .get('/retropilot/0/useradmin') + .get('/api/useradmin') .expect('Content-Type', /json/) .expect(200) .expect((req) => { @@ -36,7 +36,7 @@ export default (app) => { // eslint-disable-next-line no-empty } catch (ignored) { } - throw new Error('Invalid returned parameters in GET /retropilot/0/useradmin '); + throw new Error('Invalid returned parameters in GET /api/useradmin '); }) .end(done); });