/* eslint-disable */ // TODO: delete useradmin... import express from 'express'; import bodyParser from 'body-parser'; import crypto from 'crypto'; import htmlspecialchars from 'htmlspecialchars'; import dirTree from 'directory-tree'; import cookieParser from 'cookie-parser'; import log4js from 'log4js'; import authenticationController from '../controllers/authentication'; 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(); let models; const router = express.Router(); // TODO Remove this, pending on removing all auth logic from routes router.use(cookieParser()); function runAsyncWrapper(callback) { return function wrapper(req, res, next) { callback(req, res, next) .catch(next); }; } if (process.env.NODE_ENV === 'development') { router.get('/useradmin/createbaseaccount', runAsyncWrapper(async (req, res) => { res.send(await userController.createBaseAccount()); })); } router.post('/useradmin/auth', bodyParser.urlencoded({ extended: true }), runAsyncWrapper(async (req, res) => { const signIn = await authenticationController.signIn(req.body.email, req.body.password); logger.info(signIn); if (signIn.success) { res.cookie('jwt', signIn.jwt); res.redirect('/useradmin/overview'); } else { res.redirect(`/useradmin?status=${encodeURIComponent('Invalid credentials or banned account')}`); } })); router.get('/useradmin/signout', runAsyncWrapper(async (req, res) => { res.clearCookie('session'); res.clearCookie('jwt'); res.redirect(`/useradmin?status=${encodeURIComponent('Signed out')}`); })); router.get('/useradmin', runAsyncWrapper(async (req, res) => { const account = await authenticationController.getAuthenticatedAccount(req); if (account != null) { res.redirect('/useradmin/overview'); return; } /* TODO reimplement const accounts = await models.get('SELECT COUNT(*) AS num FROM accounts'); const devices = await models.get('SELECT COUNT(*) AS num FROM devices'); const drives = await models.get('SELECT COUNT(*) AS num, SUM(distance_meters) as distance, SUM(duration) as duration FROM drives'); */ res.status(200); res.send(`

Welcome To The RetroPilot Server Dashboard!



Login

${req.query.status !== undefined ? `${htmlspecialchars(req.query.status)}
` : ''}


${!process.env.ALLOW_REGISTRATION ? 'User Account Registration is disabled on this Server' : 'Register new Account'}



${process.env.WELCOME_MESSAGE} `, /* Accounts: ${accounts.num} | Devices: ${devices.num} | Drives: ${drives.num} | Distance Traveled: ${Math.round(drives.distance / 1000)} km | Time Traveled: ${helperController.formatDuration(drives.duration)} | Storage Used: ${await storageController.getTotalStorageUsed() !== null ? await storageController.getTotalStorageUsed() : '--'}

${process.env.WELCOME_MESSAGE}` */); })); router.post('/useradmin/register/token', bodyParser.urlencoded({ extended: true }), runAsyncWrapper(async (req, res) => { const { email } = req.body; if (!email) { logger.warn('/useradmin/register/token - Malformed Request!'); return res.status(400).send('Malformed Request'); } if (!process.env.ALLOW_REGISTRATION) { return res.status(401).send('Unauthorized.'); } const authAccount = await authenticationController.getAuthenticatedAccount(req); if (authAccount != null) { return res.redirect('/useradmin/overview'); } const account = await userController.getAccountFromEmail(email.trim().toLowerCase()); if (account != null) { return res.redirect(`/useradmin/register?status=${encodeURIComponent('Email is already registered')}`); } const token = (process.env.NODE_ENV === 'development') ? 'verysecrettoken' : 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.

'; await mailingController.sendEmailVerification(token, email); } else 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 match or were shorter than 3 characters, please try again.

'; } else { let result = false; try { result = await userController._dirtyCreateAccount( email, crypto.createHash('sha256').update(req.body.password + process.env.APP_SALT).digest('hex'), Date.now(), false, ); } catch (error) { console.error('error creating account', error); } logger.debug('created account:', result); if (result.dataValues) { const account = result.dataValues; logger.info(`USERADMIN REGISTRATION - created new account #${account.id} with email ${email}`); return res.redirect(`/useradmin?status=${encodeURIComponent('Successfully registered')}`); } logger.error(`USERADMIN REGISTRATION - account creation failed, resulting account data for email ${email} is: ${result}`); infoText = 'Unable to complete account registration (database error).

'; } return res.status(200).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) { return res.status(400).send('Unauthorized.'); } const account = await authenticationController.getAuthenticatedAccount(req); if (account != null) { return res.redirect('/useradmin/overview'); } return res.status(200).send(`

Welcome To The RetroPilot Server Dashboard!

< < < Back To Login

Register / Request Email Token

${req.query.status !== undefined ? `${htmlspecialchars(req.query.status)}
` : ''}
`); })); router.get('/useradmin/overview', runAsyncWrapper(async (req, res) => { let account = await authenticationController.getAuthenticatedAccount(req); if (account === null) { return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid or expired session')}`); } account = account.dataValues; const devices = await deviceController.getDevices(account.id); let response = `

Welcome To The RetroPilot Server Dashboard!



Account Overview

Account: #${account.id}
Email: ${account.email}
Created: ${helperController.formatDate(account.created)}

Devices:
`; // add each device to the table of dongles devices.forEach((device) => { response += ``; }); response += `
dongle_iddevice_typecreatedlast_pingstorage_used
${device.dongle_id} ${device.device_type} ${helperController.formatDate(device.created)} ${helperController.formatDate(device.last_ping)} ${device.storage_used} MB


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)}

` : ''}



Sign Out`; response += `
${process.env.WELCOME_MESSAGE}`; return res.status(200).send(response); })); 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')}`); } return res.redirect('/useradmin/overview'); })); 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.qrString); if (pairDevice.success === true) { res.redirect('/useradmin/overview'); } else if (pairDevice.registered === true) { res.redirect(`/useradmin/overview?linkstatus=${encodeURIComponent('Device not registered on Server')}`); } else if (pairDevice.badToken === true) { res.redirect(`/useradmin/overview?linkstatus=${encodeURIComponent('Device QR Token is invalid or has expired')}`); } else if (pairDevice.alreadyPaired) { res.redirect(`/useradmin/overview?linkstatus=${encodeURIComponent('Device is already paired, unpair in that account first')}`); } else if (pairDevice.badQr) { res.redirect(`/useradmin/overview?linkstatus=${encodeURIComponent('Bad QR')}`); } else { res.redirect(`/useradmin/overview?linkstatus=${encodeURIComponent(`Unspecified Error ${JSON.stringify(pairDevice)}`)}`); } })); router.get('/useradmin/device/:dongleId', runAsyncWrapper(async (req, res) => { const { dongleId } = req.params; const account = await authenticationController.getAuthenticatedAccount(req); if (account == null) { return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid or expired session')}`); } const device = await deviceController.getDeviceFromDongleId(req.params.dongleId); if (device == null || device.account_id !== account.id) { return res.status(400).send('Unauthorized.'); } const drives = await deviceController.getDrives(device.dongle_id, false, true); const dongleIdHash = crypto.createHmac('sha256', process.env.APP_SALT).update(device.dongle_id).digest('hex'); const bootlogFiles = await deviceController.getBootlogs(device.dongle_id); const crashlogFiles = await deviceController.getCrashlogs(device.dongle_id); let 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: ${helperController.formatDate(device.created)}
Last Ping: ${helperController.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 (let i = 0; i < Math.min(5, bootlogFiles.length); i++) { response += ``; } response += '
datefilesize
${helperController.formatDate(bootlogFiles[i].date)}${bootlogFiles[i].name}${bootlogFiles[i].size}


'; response += `Crash Logs (last 5):
`; for (let i = 0; i < Math.min(5, crashlogFiles.length); i++) { response += `. `; } response += '
datefilesize
${helperController.formatDate(crashlogFiles[i].date)}${crashlogFiles[i].name} ${crashlogFiles[i].size}


'; response += `Drives (non-preserved drives expire ${process.env.DEVICE_EXPIRATION_DAYS} days after upload):
`; // add each drive to the table drives.forEach((drive) => { let vehicle = ''; let version = ''; let metadata = {}; try { metadata = JSON.parse(drive.metadata); if (metadata.InitData && metadata.InitData.Version) { version = htmlspecialchars(metadata.InitData.Version); } if (metadata.CarParams) { if (metadata.CarParams.CarName) { vehicle += `${htmlspecialchars(metadata.CarParams.CarName.toUpperCase())} `; } if (metadata.CarParams.CarFingerprint) { vehicle += htmlspecialchars(metadata.CarParams.CarFingerprint.toUpperCase()); } } } catch (exception) { // do nothing } response += ``; }); response += `
identifier car version filesize duration distance_meters upload_complete is_processed upload_date actions
${drive.is_preserved ? '' : ''}${drive.identifier}${drive.is_preserved ? '' : ''} ${vehicle} ${version} ${Math.round(drive.filesize / 1024)} MiB ${helperController.formatDuration(drive.duration)} ${Math.round(drive.distance_meters / 1000)} km ${drive.upload_complete} ${drive.is_processed} ${helperController.formatDate(drive.created)} [delete] ${drive.is_preserved ? '' : `[preserve]`}


Unpair Device


Sign Out `; return res.status(200).send(response); })); router.get('/useradmin/drive/:dongleId/:driveIdentifier/:action', runAsyncWrapper(async (req, res) => { const account = await authenticationController.getAuthenticatedAccount(req); if (account == null) { return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid or expired session')}`); } const drive = await deviceController.getDrive(req.params.dongleId, req.params.driveIdentifier); if (drive == null) { return res.status(400).send('Unauthorized.'); } const { action } = req.params; if (action === 'delete') { await deviceController.updateOrCreateDrive(req.params.dongleId, drive.id, { is_deleted: true, }); } else if (action === 'preserve') { await deviceController.updateOrCreateDrive(req.params.dongleId, drive.id, { is_preserved: true, }); } return res.redirect(`/useradmin/device/${device.dongle_id}`); })); router.get('/useradmin/drive/:dongleId/:driveIdentifier', runAsyncWrapper(async (req, res) => { const account = await authenticationController.getAuthenticatedAccount(req); if (account == null) { return res.redirect(`/useradmin?status=${encodeURIComponent('Invalid or expired session')}`); } const device = await deviceController.getDeviceFromDongleId(req.params.dongleId); if (device == null || device.account_id !== account.id) { return res.status(400).send('Unauthorized.'); } const drive = await deviceController.getDrive(req.params.dongleId, req.params.driveIdentifier); if (drive == null) { return res.status(400).send('Unauthorized.'); } const dongleIdHash = crypto.createHmac('sha256', process.env.APP_SALT).update(device.dongle_id).digest('hex'); const driveIdentifierHash = crypto.createHmac('sha256', process.env.APP_SALT).update(drive.identifier).digest('hex'); const driveUrl = `${process.env.BASE_DRIVE_DOWNLOAD_URL + device.dongle_id}/${dongleIdHash}/${driveIdentifierHash}/${drive.identifier}/`; let 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"`; } let vehicle = ''; let version = ''; let gitRemote = ''; let gitBranch = ''; let gitCommit = ''; let metadata = {}; let carParams = ''; try { metadata = JSON.parse(drive.metadata); if (metadata.InitData) { if (metadata.InitData.Version) { version = htmlspecialchars(metadata.InitData.Version); } if (metadata.InitData.GitRemote) { gitRemote = htmlspecialchars(metadata.InitData.GitRemote); } if (metadata.InitData.GitBranch) { gitBranch = htmlspecialchars(metadata.InitData.GitBranch); } if (metadata.InitData.GitCommit) { gitCommit = htmlspecialchars(metadata.InitData.GitCommit); } } if (metadata.CarParams) { if (metadata.CarParams.CarName) { vehicle += `${htmlspecialchars(metadata.CarParams.CarName.toUpperCase())} `; } if (metadata.CarParams.CarFingerprint) { vehicle += htmlspecialchars(metadata.CarParams.CarFingerprint.toUpperCase()); } carParams = JSON.stringify(metadata.CarParams, null, 2).replace(/\r?\n|\r/g, '
'); } } catch (exception) { // do nothing } let response = `

Welcome To The RetroPilot Server Dashboard!

< < < Back To Device ${device.dongle_id}

Drive ${drive.identifier} on ${drive.dongle_id}

Drive Date: ${helperController.formatDate(drive.drive_date)}
Upload Date: ${helperController.formatDate(drive.created)}

Vehicle: ${vehicle}
Openpilot Version: ${version}

GIT Remote: ${gitRemote}
GIT Branch: ${gitBranch}
GIT Commit: ${gitCommit}

Num Segments: ${drive.max_segment + 1}
Storage: ${Math.round(drive.filesize / 1024)} MiB
Duration: ${helperController.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}

Car Parameters: Show

Preview : ${cabanaUrl ? ` ` : '(available after processing)'}
${cabanaUrl ? `View Drive in CABANA` : 'View Drive in CABANA'}

Files:
`; const directoryTree = dirTree(process.env.STORAGE_PATH + device.dongle_id + "/" + dongleIdHash + "/" + driveIdentifierHash + "/" + drive.identifier); const 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 = '' + driveUrl + segment + '' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + ''; if (directoryTree.children[i].children[c].name == 'dcamera.hevc') fcamera = '' + driveUrl + segment + '' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + ''; if (directoryTree.children[i].children[c].name == 'qcamera.ts') qcamera = '' + driveUrl + segment + '' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + ''; if (directoryTree.children[i].children[c].name == 'qlog.bz2') qlog = '' + driveUrl + segment + '' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + ''; if (directoryTree.children[i].children[c].name == 'rlog.bz2') rlog = '' + driveUrl + segment + '' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + ''; } let isProcessed = '?'; let 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;