2022-03-20 17:59:37 -06:00
/* eslint-disable */
// TODO: delete useradmin...
2022-01-12 08:02:30 -07:00
import bodyParser from 'body-parser' ;
2022-03-24 15:21:08 -06:00
import cookieParser from 'cookie-parser' ;
2022-01-12 08:02:30 -07:00
import crypto from 'crypto' ;
import dirTree from 'directory-tree' ;
2022-03-24 15:21:08 -06:00
import express from 'express' ;
import htmlspecialchars from 'htmlspecialchars' ;
2022-01-12 08:02:30 -07:00
import log4js from 'log4js' ;
2022-03-24 15:03:19 -06:00
2022-01-12 08:02:30 -07:00
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' ;
2022-03-24 15:03:19 -06:00
import { getAccount } from '../middlewares/authentication' ;
2022-03-24 18:40:07 -06:00
import { getDevice } from '../middlewares/devices' ;
2022-01-09 16:19:00 -07:00
2022-03-24 15:03:19 -06:00
const logger = log4js . getLogger ( 'useradmin' ) ;
2022-01-12 08:02:30 -07:00
const router = express . Router ( ) ;
2022-01-03 09:29:47 -07:00
// TODO Remove this, pending on removing all auth logic from routes
router . use ( cookieParser ( ) ) ;
2021-05-21 16:17:11 -06:00
function runAsyncWrapper ( callback ) {
2022-01-08 19:23:14 -07:00
return function wrapper ( req , res , next ) {
2022-01-07 18:35:55 -07:00
callback ( req , res , next )
. catch ( next ) ;
} ;
2021-05-21 16:17:11 -06:00
}
2022-03-24 15:03:19 -06:00
const requireAuthenticated = async ( req , res , next ) => {
const account = await authenticationController . getAuthenticatedAccount ( req ) ;
if ( account == null ) {
return res . redirect ( ` /useradmin?status= ${ encodeURIComponent ( 'Invalid or expired session' ) } ` ) ;
} else {
req . account = account ;
return next ( ) ;
}
} ;
2022-03-22 07:03:17 -06:00
if ( process . env . NODE _ENV === 'development' ) {
2022-03-23 16:43:46 -06:00
router . get ( '/createbaseaccount' , runAsyncWrapper ( async ( req , res ) => {
2022-03-02 20:37:16 -07:00
res . send ( await userController . createBaseAccount ( ) ) ;
} ) ) ;
}
2022-03-23 16:43:46 -06:00
router . post ( '/auth' , bodyParser . urlencoded ( { extended : true } ) , runAsyncWrapper ( async ( req , res ) => {
2022-01-09 16:19:00 -07:00
const signIn = await authenticationController . signIn ( req . body . email , req . body . password ) ;
2021-05-21 16:17:11 -06:00
2022-03-21 17:38:56 -06:00
logger . info ( signIn ) ;
2021-05-21 16:17:11 -06:00
2022-01-07 18:35:55 -07:00
if ( signIn . success ) {
res . cookie ( 'jwt' , signIn . jwt ) ;
res . redirect ( '/useradmin/overview' ) ;
2022-01-08 15:00:08 -07:00
} else {
2022-01-07 18:35:55 -07:00
res . redirect ( ` /useradmin?status= ${ encodeURIComponent ( 'Invalid credentials or banned account' ) } ` ) ;
}
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-23 16:43:46 -06:00
router . get ( '/signout' , runAsyncWrapper ( async ( req , res ) => {
2022-01-07 18:35:55 -07:00
res . clearCookie ( 'session' ) ;
res . clearCookie ( 'jwt' ) ;
res . redirect ( ` /useradmin?status= ${ encodeURIComponent ( 'Signed out' ) } ` ) ;
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
router . get ( '/' , getAccount , runAsyncWrapper ( async ( req , res ) => {
const { account } = req ;
2022-03-24 15:15:22 -06:00
if ( account ) {
2022-01-07 18:35:55 -07:00
res . redirect ( '/useradmin/overview' ) ;
return ;
}
2021-05-21 16:17:11 -06:00
2022-01-09 19:49:24 -07:00
/ * T O D O r e i m p l e m e n t
2022-01-09 16:19:00 -07:00
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' ) ;
2021-05-21 16:17:11 -06:00
2022-01-09 19:49:24 -07:00
* /
2022-01-07 18:35:55 -07:00
res . status ( 200 ) ;
2022-01-08 15:01:13 -07:00
res . send ( ` <html style="font-family: monospace">
< h2 > Welcome To The RetroPilot Server Dashboard ! < / h 2 >
< br > < br >
< h3 > Login < / h 3 >
2022-03-24 18:46:32 -06:00
$ { req . query . status ? ` <u> ${ htmlspecialchars ( req . query . status ) } </u><br> ` : '' }
2022-01-08 15:01:13 -07:00
< form action = "/useradmin/auth" method = "POST" >
< input type = "email" name = "email" placeholder = "Email" required >
< input type = "password" name = "password" placeholder = "Password" required >
< input type = "submit" >
< / f o r m >
< br > < br >
2022-02-28 22:04:36 -07:00
$ { ! process . env . ALLOW _REGISTRATION ? '<i>User Account Registration is disabled on this Server</i>' : '<a href="/useradmin/register">Register new Account</a>' }
2022-01-08 15:01:13 -07:00
< br > < br >
2022-02-28 22:04:36 -07:00
< br > < br > $ { process . env . WELCOME _MESSAGE }
2022-01-09 19:49:24 -07:00
< / h t m l >
` , /*
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 ( ) : '--' }
2022-02-28 22:04:36 -07:00
< br > < br > $ { process . env . WELCOME _MESSAGE } ` */);
2022-01-07 18:35:55 -07:00
} ) ) ;
2022-03-23 16:43:46 -06:00
router . post ( '/register/token' , bodyParser . urlencoded ( { extended : true } ) , runAsyncWrapper ( async ( req , res ) => {
2022-01-08 19:23:14 -07:00
const { email } = req . body ;
if ( ! email ) {
2022-01-07 18:35:55 -07:00
logger . warn ( '/useradmin/register/token - Malformed Request!' ) ;
2022-01-08 19:23:14 -07:00
return res . status ( 400 ) . send ( 'Malformed Request' ) ;
2022-01-07 18:35:55 -07:00
}
2022-02-28 22:04:36 -07:00
if ( ! process . env . ALLOW _REGISTRATION ) {
2022-03-24 16:05:05 -06:00
return res . status ( 401 ) . send ( 'Unauthorised.' ) ;
2022-01-07 18:35:55 -07:00
}
2022-01-09 16:19:00 -07:00
const authAccount = await authenticationController . getAuthenticatedAccount ( req ) ;
2022-03-24 15:15:22 -06:00
if ( authAccount ) {
2022-01-08 19:23:14 -07:00
return res . redirect ( '/useradmin/overview' ) ;
2022-01-07 18:35:55 -07:00
}
2021-05-21 16:17:11 -06:00
2022-01-09 19:49:24 -07:00
const account = await userController . getAccountFromEmail ( email . trim ( ) . toLowerCase ( ) ) ;
2022-03-24 15:15:22 -06:00
if ( account ) {
2022-01-08 19:23:14 -07:00
return res . redirect ( ` /useradmin/register?status= ${ encodeURIComponent ( 'Email is already registered' ) } ` ) ;
2022-01-07 18:35:55 -07:00
}
2021-05-21 16:17:11 -06:00
2022-03-24 15:15:22 -06:00
const token = ( process . env . NODE _ENV === 'development' )
? 'verysecrettoken'
: crypto . createHmac ( 'sha256' , process . env . APP _SALT ) . update ( email . trim ( ) ) . digest ( 'hex' ) ;
2021-05-21 16:17:11 -06:00
2022-01-07 18:35:55 -07:00
let infoText = '' ;
2021-05-21 16:17:11 -06:00
2022-03-24 07:51:39 -06:00
if ( ! req . body . token ) { // email entered, token request
2022-01-07 18:35:55 -07:00
infoText = 'Please check your inbox (<b>SPAM</b>) for an email with the registration token.<br>If the token was not delivered, please ask the administrator to check the <i>server.log</i> for the token generated for your email.<br><br>' ;
2021-05-21 16:17:11 -06:00
2022-03-02 18:03:32 -07:00
await mailingController . sendEmailVerification ( token , email ) ;
2022-01-08 19:23:14 -07:00
} else if ( req . body . token !== token ) {
infoText = 'The registration token you entered was incorrect, please try again.<br><br>' ;
} 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.<br><br>' ;
} else {
2022-03-24 15:15:22 -06:00
let result ;
2022-03-02 19:18:07 -07:00
try {
result = await userController . _dirtyCreateAccount (
email ,
crypto . createHash ( 'sha256' ) . update ( req . body . password + process . env . APP _SALT ) . digest ( 'hex' ) ,
Date . now ( ) ,
false ,
) ;
2022-03-21 17:38:56 -06:00
} catch ( error ) {
console . error ( 'error creating account' , error ) ;
2022-03-02 19:18:07 -07:00
}
2022-01-07 18:35:55 -07:00
2022-03-21 17:38:56 -06:00
logger . debug ( 'created account:' , result ) ;
2022-01-12 08:02:30 -07:00
if ( result . dataValues ) {
2022-03-24 15:21:08 -06:00
const newAccount = result . dataValues ;
logger . info ( ` USERADMIN REGISTRATION - created new account # ${ newAccount . id } with email ${ email } ` ) ;
2022-01-08 19:23:14 -07:00
return res . redirect ( ` /useradmin?status= ${ encodeURIComponent ( 'Successfully registered' ) } ` ) ;
2022-01-07 18:35:55 -07:00
}
2022-03-02 19:18:07 -07:00
2022-01-08 19:23:14 -07:00
logger . error ( ` USERADMIN REGISTRATION - account creation failed, resulting account data for email ${ email } is: ${ result } ` ) ;
infoText = 'Unable to complete account registration (database error).<br><br>' ;
2022-01-07 18:35:55 -07:00
}
2021-05-21 16:17:11 -06:00
2022-01-08 19:23:14 -07:00
return res . status ( 200 ) . send ( ` <html style="font-family: monospace">
2022-01-08 15:01:13 -07:00
< h2 > Welcome To The RetroPilot Server Dashboard ! < / h 2 >
< a href = "/useradmin" > < < < Back To Login < / a >
< br > < br >
< h3 > Register / Finish Registration < / h 3 >
$ { infoText }
< form action = "/useradmin/register/token" method = "POST" >
< input type = "email" name = "email" placeholder = "Email" value = "${htmlspecialchars(email.trim())}" required >
< input type = "text" name = "token" placeholder = "Email Token" value = "${req.body.token ? htmlspecialchars(req.body.token.trim()) : ''}" required > < br >
< input type = "password" name = "password" placeholder = "Password" value = "${req.body.password ? htmlspecialchars(req.body.password.trim()) : ''}" required >
< input type = "password" name = "password2" placeholder = "Repeat Password" value = "${req.body.password2 ? htmlspecialchars(req.body.password2.trim()) : ''}" required >
< input type = "submit" value = "Finish Registration" >
< / f o r m >
< / h t m l > ` ) ;
2022-01-07 18:35:55 -07:00
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
router . get ( '/register' , getAccount , runAsyncWrapper ( async ( req , res ) => {
2022-02-28 22:04:36 -07:00
if ( ! process . env . ALLOW _REGISTRATION ) {
2022-03-24 16:05:05 -06:00
return res . status ( 401 ) . send ( 'Unauthorised.' ) ;
2022-01-07 18:35:55 -07:00
}
2022-03-24 15:03:19 -06:00
if ( req . account ) {
2022-01-08 19:23:14 -07:00
return res . redirect ( '/useradmin/overview' ) ;
2022-01-07 18:35:55 -07:00
}
2021-05-21 16:17:11 -06:00
2022-01-08 19:23:14 -07:00
return res . status ( 200 ) . send ( ` <html style="font-family: monospace">
2022-01-08 15:01:13 -07:00
< h2 > Welcome To The RetroPilot Server Dashboard ! < / h 2 >
< a href = "/useradmin" > < < < Back To Login < / a >
< br > < br >
< h3 > Register / Request Email Token < / h 3 >
$ { req . query . status !== undefined ? ` <u> ${ htmlspecialchars ( req . query . status ) } </u><br> ` : '' }
< form action = "/useradmin/register/token" method = "POST" >
< input type = "email" name = "email" placeholder = "Email" required >
< input type = "submit" value = "Verify Email" >
< / f o r m >
< / h t m l > ` ) ;
2022-01-07 18:35:55 -07:00
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
router . get ( '/overview' , requireAuthenticated , runAsyncWrapper ( async ( req , res ) => {
const { account } = req ;
2022-01-09 19:49:24 -07:00
const devices = await deviceController . getDevices ( account . id ) ;
2021-05-21 16:17:11 -06:00
2022-01-08 15:01:13 -07:00
let response = ` <html style="font-family: monospace">
< h2 > Welcome To The RetroPilot Server Dashboard ! < / h 2 >
2022-03-24 18:40:07 -06:00
$ { req . query . status ? ` <dialog open> ${ htmlspecialchars ( req . query . status ) } </dialog> ` : '' }
2022-01-08 15:01:13 -07:00
< br > < br >
< h3 > Account Overview < / h 3 >
< b > Account : < / b > # $ { a c c o u n t . i d } < b r >
< b > Email : < / b > $ { a c c o u n t . e m a i l } < b r >
2022-01-09 16:19:00 -07:00
< b > Created : < / b > $ { h e l p e r C o n t r o l l e r . f o r m a t D a t e ( a c c o u n t . c r e a t e d ) } < b r > < b r >
2022-01-08 15:01:13 -07:00
< b > Devices : < / b > < b r >
< table border = 1 cellpadding = 2 cellspacing = 2 >
< tr > < th > dongle _id < / t h > < t h > d e v i c e _ t y p e < / t h > < t h > c r e a t e d < / t h > < t h > l a s t _ p i n g < / t h > < t h > s t o r a g e _ u s e d < / t h > < / t r >
` ;
2022-01-08 19:23:14 -07:00
// add each device to the table of dongles
devices . forEach ( ( device ) => {
2022-01-08 15:01:13 -07:00
response += ` <tr>
2022-01-08 19:23:14 -07:00
< td > < a href = "/useradmin/device/${device.dongle_id}" > $ { device . dongle _id } < / a > < / t d >
< td > $ { device . device _type } < / t d >
2022-01-09 16:19:00 -07:00
< td > $ { helperController . formatDate ( device . created ) } < / t d >
< td > $ { helperController . formatDate ( device . last _ping ) } < / t d >
2022-01-08 19:23:14 -07:00
< td > $ { device . storage _used } MB < / t d >
2022-01-08 15:01:13 -07:00
< / t r > ` ;
2022-01-08 19:23:14 -07:00
} ) ;
2022-01-07 18:35:55 -07:00
response += ` </table>
2022-01-08 15:01:13 -07:00
< br >
< hr / >
< h3 > Pair New Devices < / h 3 >
< i > * To pair a new device , first have it auto - register on this server . < br > Then scan the QR Code and paste the Device Token below . < / i > < b r >
$ { req . query . linkstatus !== undefined ? ` <br><u> ${ htmlspecialchars ( req . query . linkstatus ) } </u><br><br> ` : '' }
2022-03-24 18:46:32 -06:00
< form action = "/useradmin/pair_device" method = "POST" >
2022-03-22 09:14:08 -06:00
< input type = "text" name = "qrString" placeholder = "QR Code Device Token" required >
2022-01-08 15:01:13 -07:00
< input type = "submit" value = "Pair" >
< / f o r m >
< br > < br >
< hr / >
< a href = "/useradmin/signout" > Sign Out < / a > ` ;
2021-05-21 16:17:11 -06:00
2022-02-28 22:04:36 -07:00
response += ` <br> ${ process . env . WELCOME _MESSAGE } </html> ` ;
2021-05-21 16:17:11 -06:00
2022-01-08 19:23:14 -07:00
return res . status ( 200 ) . send ( response ) ;
2022-01-07 18:35:55 -07:00
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 18:40:07 -06:00
router . get ( '/unpair_device/:dongleId' , [ requireAuthenticated , getDevice ] , runAsyncWrapper ( async ( req , res ) => {
const { device } = req ;
if ( ! device ) {
return res . redirect ( ` /useradmin/overview?status= ${ encodeURIComponent ( 'Device not found' ) } ` ) ;
}
if ( device . account _id !== req . account . id ) {
return res . redirect ( ` /useradmin/overview?status= ${ encodeURIComponent ( 'Not authorized' ) } ` ) ;
}
const result = await deviceController . unpairDevice ( device . dongle _id , req . account . id ) ;
if ( ! result . success ) {
logger . warn ( ` Failed to unpair device ${ device . dongle _id } for account ${ req . account . id } : ${ result } ` ) ;
return res . redirect ( ` /useradmin/overview?status= ${ encodeURIComponent ( 'An unknown error occurred' ) } ` ) ;
}
return res . redirect ( ` /useradmin/overview?status= ${ encodeURIComponent ( 'Device unpaired successfully' ) } ` ) ;
2022-01-08 19:23:14 -07:00
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
router . post ( '/pair_device' , [ requireAuthenticated , bodyParser . urlencoded ( { extended : true } ) ] , runAsyncWrapper ( async ( req , res ) => {
2022-03-22 09:14:08 -06:00
const { account , body : { qrString } } = req ;
2021-05-21 16:17:11 -06:00
2022-03-24 07:51:39 -06:00
const pairDevice = await deviceController . pairDevice ( account , qrString ) ;
2022-01-07 18:35:55 -07:00
if ( pairDevice . success === true ) {
res . redirect ( '/useradmin/overview' ) ;
2022-01-08 15:00:08 -07:00
} else if ( pairDevice . registered === true ) {
2022-01-07 18:35:55 -07:00
res . redirect ( ` /useradmin/overview?linkstatus= ${ encodeURIComponent ( 'Device not registered on Server' ) } ` ) ;
2022-01-08 15:00:08 -07:00
} else if ( pairDevice . badToken === true ) {
2022-01-07 18:35:55 -07:00
res . redirect ( ` /useradmin/overview?linkstatus= ${ encodeURIComponent ( 'Device QR Token is invalid or has expired' ) } ` ) ;
2022-01-08 15:00:08 -07:00
} else if ( pairDevice . alreadyPaired ) {
2022-01-07 18:35:55 -07:00
res . redirect ( ` /useradmin/overview?linkstatus= ${ encodeURIComponent ( 'Device is already paired, unpair in that account first' ) } ` ) ;
2022-01-08 15:00:08 -07:00
} else if ( pairDevice . badQr ) {
2022-01-07 18:35:55 -07:00
res . redirect ( ` /useradmin/overview?linkstatus= ${ encodeURIComponent ( 'Bad QR' ) } ` ) ;
2022-01-08 15:00:08 -07:00
} else {
2022-01-07 18:35:55 -07:00
res . redirect ( ` /useradmin/overview?linkstatus= ${ encodeURIComponent ( ` Unspecified Error ${ JSON . stringify ( pairDevice ) } ` ) } ` ) ;
}
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
router . get ( '/device/:dongleId' , requireAuthenticated , runAsyncWrapper ( async ( req , res ) => {
2022-01-08 19:23:14 -07:00
const { dongleId } = req . params ;
2022-03-24 07:51:39 -06:00
const device = await deviceController . getDeviceFromDongleId ( dongleId ) ;
if ( ! device ) {
return res . status ( 404 ) . send ( 'Not Found.' ) ;
2022-03-24 15:03:19 -06:00
}
const { account _id : accountId } = device ;
if ( accountId !== req . account . id ) {
2022-03-24 16:05:05 -06:00
return res . status ( 401 ) . send ( 'Unauthorised.' ) ;
2022-01-07 18:35:55 -07:00
}
2022-03-24 15:03:19 -06:00
const drives = await deviceController . getDrives ( dongleId , false , true ) ;
2022-01-07 18:35:55 -07:00
2022-03-24 15:03:19 -06:00
const dongleIdHash = crypto . createHmac ( 'sha256' , process . env . APP _SALT ) . update ( dongleId ) . digest ( 'hex' ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
const bootlogFiles = await deviceController . getBootlogs ( dongleId ) ;
const crashlogFiles = await deviceController . getCrashlogs ( dongleId ) ;
2022-01-08 15:01:13 -07:00
let response = ` <html style="font-family: monospace">
< h2 > Welcome To The RetroPilot Server Dashboard ! < / h 2 >
< a href = "/useradmin/overview" > < < < Back To Overview < / a >
< br > < br >
2022-03-24 15:03:19 -06:00
< h3 > Device $ { dongleId } < / h 3 >
2022-01-08 15:01:13 -07:00
< b > Type : < / b > $ { d e v i c e . d e v i c e _ t y p e } < b r >
< b > Serial : < / b > $ { d e v i c e . s e r i a l } < b r >
< b > IMEI : < / b > $ { d e v i c e . i m e i } < b r >
2022-01-09 16:19:00 -07:00
< b > Registered : < / b > $ { h e l p e r C o n t r o l l e r . f o r m a t D a t e ( d e v i c e . c r e a t e d ) } < b r >
< b > Last Ping : < / b > $ { h e l p e r C o n t r o l l e r . f o r m a t D a t e ( d e v i c e . l a s t _ p i n g ) } < b r >
2022-01-08 15:01:13 -07:00
< b > Public Key : < / b > < b r >
< span style = "font-size: 0.8em" > $ { device . public _key . replace ( /\r?\n|\r/g , '<br>' ) } < / s p a n > < b r >
< b > Stored Drives : < / b > $ { d r i v e s . l e n g t h } < b r >
2022-02-28 22:04:36 -07:00
< b > Quota Storage : < /b> ${device.storage_used} MB / $ { process . env . DEVICE _STORAGE _QUOTA _MB } MB < br >
2022-01-08 15:01:13 -07:00
< br > ` ;
response += ` <b>Boot Logs (last 5):</b>
< br >
< table border = 1 cellpadding = 2 cellspacing = 2 >
< tr > < th > date < / t h > < t h > f i l e < / t h > < t h > s i z e < / t h > < / t r >
` ;
for ( let i = 0 ; i < Math . min ( 5 , bootlogFiles . length ) ; i ++ ) {
2022-03-24 15:03:19 -06:00
response += ` <tr><td> ${ helperController . formatDate ( bootlogFiles [ i ] . date ) } </td><td><a href=" ${ process . env . BASE _DRIVE _DOWNLOAD _URL } ${ dongleId } / ${ dongleIdHash } /boot/ ${ bootlogFiles [ i ] . name } " target=_blank> ${ bootlogFiles [ i ] . name } </a></td><td> ${ bootlogFiles [ i ] . size } </td></tr> ` ;
2022-01-07 18:35:55 -07:00
}
response += '</table><br><br>' ;
2021-05-21 16:17:11 -06:00
2022-01-07 18:35:55 -07:00
response += ` <b>Crash Logs (last 5):</b><br>
2021-05-21 16:17:11 -06:00
< table border = 1 cellpadding = 2 cellspacing = 2 >
2022-01-08 15:01:13 -07:00
< tr > < th > date < / t h > < t h > f i l e < / t h > < t h > s i z e < / t h > < / t r > ` ;
for ( let i = 0 ; i < Math . min ( 5 , crashlogFiles . length ) ; i ++ ) {
response += ` <tr>
2022-01-09 16:19:00 -07:00
< td > $ { helperController . formatDate ( crashlogFiles [ i ] . date ) } < / t d > .
2022-03-24 15:03:19 -06:00
< td > < a href = "${process.env.BASE_DRIVE_DOWNLOAD_URL}${dongleId}/${dongleIdHash}/crash/${crashlogFiles[i].name}" target = _blank > $ { crashlogFiles [ i ] . name } < / a > < / t d >
2022-01-08 15:01:13 -07:00
< td > $ { crashlogFiles [ i ] . size } < / t d >
< / t r > ` ;
2022-01-07 18:35:55 -07:00
}
response += '</table><br><br>' ;
2021-05-21 16:17:11 -06:00
2022-02-28 22:04:36 -07:00
response += ` <b>Drives (non-preserved drives expire ${ process . env . DEVICE _EXPIRATION _DAYS } days after upload):</b><br>
2021-05-21 16:17:11 -06:00
< table border = 1 cellpadding = 2 cellspacing = 2 >
2022-01-08 15:01:13 -07:00
< tr >
< th > identifier < / t h >
< th > car < / t h >
< th > version < / t h >
< th > filesize < / t h >
< th > duration < / t h >
< th > distance _meters < / t h >
< th > upload _complete < / t h >
< th > is _processed < / t h >
< th > upload _date < / t h >
< th > actions < / t h >
< / t r > ` ;
2022-01-08 19:23:14 -07:00
// add each drive to the table
drives . forEach ( ( drive ) => {
2022-01-07 18:35:55 -07:00
let vehicle = '' ;
let version = '' ;
let metadata = { } ;
try {
2022-01-08 19:23:14 -07:00
metadata = JSON . parse ( drive . metadata ) ;
2022-01-08 15:01:13 -07:00
if ( metadata . InitData && metadata . InitData . Version ) {
2022-01-07 18:35:55 -07:00
version = htmlspecialchars ( metadata . InitData . Version ) ;
}
2022-01-08 15:01:13 -07:00
if ( metadata . CarParams ) {
if ( metadata . CarParams . CarName ) {
vehicle += ` ${ htmlspecialchars ( metadata . CarParams . CarName . toUpperCase ( ) ) } ` ;
}
if ( metadata . CarParams . CarFingerprint ) {
vehicle += htmlspecialchars ( metadata . CarParams . CarFingerprint . toUpperCase ( ) ) ;
}
2022-01-07 18:35:55 -07:00
}
2022-01-08 19:23:14 -07:00
} catch ( exception ) {
// do nothing
}
response += ` <tr>
2022-03-24 15:03:19 -06:00
< td > < a href = "/useradmin/drive/${dongleId}/${drive.identifier}" > $ { drive . is _preserved ? '<b>' : '' } $ { drive . identifier } $ { drive . is _preserved ? '</b>' : '' } < / a > < / t d >
2022-01-08 19:23:14 -07:00
< td > $ { vehicle } < / t d >
< td > $ { version } < / t d >
< td > $ { Math . round ( drive . filesize / 1024 ) } MiB < / t d >
2022-01-09 16:19:00 -07:00
< td > $ { helperController . formatDuration ( drive . duration ) } < / t d >
2022-01-08 19:23:14 -07:00
< td > $ { Math . round ( drive . distance _meters / 1000 ) } km < / t d >
< td > $ { drive . upload _complete } < / t d >
< td > $ { drive . is _processed } < / t d >
2022-01-09 16:19:00 -07:00
< td > $ { helperController . formatDate ( drive . created ) } < / t d >
2022-01-08 19:23:14 -07:00
< td >
2022-03-24 15:03:19 -06:00
[ < a href = "/useradmin/drive/${dongleId}/${drive.identifier}/delete" onclick = "return confirm('Permanently delete this drive?')" > delete < / a > ]
$ { drive . is _preserved ? '' : ` [<a href="/useradmin/drive/ ${ dongleId } / ${ drive . identifier } /preserve">preserve</a>] ` }
2022-01-08 19:23:14 -07:00
< / t d >
< / t r > ` ;
} ) ;
2022-01-07 18:35:55 -07:00
2022-01-08 15:01:13 -07:00
response += ` </table>
< br >
< hr / >
2022-03-24 15:03:19 -06:00
< a href = "/useradmin/unpair_device/${dongleId}" onclick = "return confirm('Are you sure that you want to unpair your device? Uploads will be rejected until it is paired again.')" > Unpair Device < / a >
2022-01-08 15:01:13 -07:00
< br > < br >
< hr / >
< a href = "/useradmin/signout" > Sign Out < / a >
< / h t m l > ` ;
2021-05-21 16:17:11 -06:00
2022-01-08 19:23:14 -07:00
return res . status ( 200 ) . send ( response ) ;
2022-01-07 18:35:55 -07:00
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
router . get ( '/drive/:dongleId/:driveIdentifier' , requireAuthenticated , runAsyncWrapper ( async ( req , res ) => {
const { dongleId } = req . params ;
const device = await deviceController . getDeviceFromDongleId ( dongleId ) ;
if ( ! device ) {
2022-03-24 17:34:13 -06:00
logger . debug ( 'HTTP.DRIVE Device not found, dongleId:' , dongleId ) ;
2022-03-24 07:51:39 -06:00
return res . status ( 404 ) . send ( 'Not Found.' ) ;
2022-01-07 18:35:55 -07:00
}
2022-03-24 15:03:19 -06:00
const { account _id : accountId } = device ;
if ( accountId !== req . account . id ) {
2022-03-24 17:34:13 -06:00
logger . debug ( 'HTTP.DRIVE Account mismatch' ) ;
2022-03-24 15:03:19 -06:00
return res . status ( 403 ) . send ( 'Forbidden.' ) ;
2022-01-07 18:35:55 -07:00
}
2022-03-24 07:51:39 -06:00
2022-03-24 15:03:19 -06:00
const { driveIdentifier } = req . params ;
2022-03-24 17:34:13 -06:00
const drive = await deviceController . getDriveFromIdentifier ( dongleId , driveIdentifier ) ;
if ( ! drive ) {
logger . debug ( 'HTTP.DRIVE Drive not found, dongleId:' , dongleId , 'driveIdentifier:' , driveIdentifier ) ;
2022-03-24 07:51:39 -06:00
return res . status ( 404 ) . send ( 'Not Found.' ) ;
2022-01-07 18:35:55 -07:00
}
2022-03-24 15:03:19 -06:00
const dongleIdHash = crypto
. createHmac ( 'sha256' , process . env . APP _SALT )
. update ( dongleId )
. digest ( 'hex' ) ;
const driveIdentifierHash = crypto
. createHmac ( 'sha256' , process . env . APP _SALT )
. update ( driveIdentifier )
. digest ( 'hex' ) ;
2022-01-07 18:35:55 -07:00
2022-03-24 15:03:19 -06:00
const driveUrl = ` ${ process . env . BASE _DRIVE _DOWNLOAD _URL + dongleId } / ${ dongleIdHash } / ${ driveIdentifierHash } / ${ driveIdentifier } / ` ;
2022-01-07 18:35:55 -07:00
2022-01-08 15:00:08 -07:00
let cabanaUrl = null ;
2022-01-07 18:35:55 -07:00
if ( drive . is _processed ) {
2022-03-24 15:03:19 -06:00
cabanaUrl = ` ${ process . env . CABANA _URL } ?retropilotIdentifier= ${ dongleId } | ${ dongleIdHash } | ${ driveIdentifier } | ${ driveIdentifierHash } &retropilotHost= ${ encodeURIComponent ( process . env . BASE _URL ) } &demo=1" ` ;
2022-01-07 18:35:55 -07:00
}
let vehicle = '' ;
let version = '' ;
let gitRemote = '' ;
let gitBranch = '' ;
let gitCommit = '' ;
let metadata = { } ;
2022-01-08 19:23:14 -07:00
let carParams = '' ;
2022-01-07 18:35:55 -07:00
try {
metadata = JSON . parse ( drive . metadata ) ;
2022-01-08 15:01:13 -07:00
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 ) ;
}
2021-05-21 16:17:11 -06:00
}
2022-01-08 15:01:13 -07:00
if ( metadata . CarParams ) {
if ( metadata . CarParams . CarName ) {
vehicle += ` ${ htmlspecialchars ( metadata . CarParams . CarName . toUpperCase ( ) ) } ` ;
}
if ( metadata . CarParams . CarFingerprint ) {
vehicle += htmlspecialchars ( metadata . CarParams . CarFingerprint . toUpperCase ( ) ) ;
}
2021-05-21 16:17:11 -06:00
2022-01-08 19:23:14 -07:00
carParams = JSON . stringify ( metadata . CarParams , null , 2 ) . replace ( /\r?\n|\r/g , '<br>' ) ;
}
} catch ( exception ) {
// do nothing
}
2021-05-21 16:17:11 -06:00
2022-01-08 15:01:13 -07:00
let response = ` <html style="font-family: monospace">
< head >
< link href = "https://vjs.zencdn.net/7.11.4/video-js.css" rel = "stylesheet" / >
< script src = "https://vjs.zencdn.net/7.11.4/video.min.js" > < / s c r i p t >
< style >
. video - js . vjs - current - time ,
. vjs - no - flex . vjs - current - time {
display : block ;
}
. vjs - default - skin . vjs - paused . vjs - big - play - button { display : none ; }
< / s t y l e >
< / h e a d >
< body >
< h2 > Welcome To The RetroPilot Server Dashboard ! < / h 2 >
2022-03-24 15:03:19 -06:00
< a href = "/useradmin/device/${dongleId}" > < < < Back To Device $ { dongleId } < / a >
< br > < br > < h3 > Drive $ { driveIdentifier } on $ { dongleId } < / h 3 >
2022-01-09 16:19:00 -07:00
< b > Drive Date : < / b > $ { h e l p e r C o n t r o l l e r . f o r m a t D a t e ( d r i v e . d r i v e _ d a t e ) } < b r >
< b > Upload Date : < / b > $ { h e l p e r C o n t r o l l e r . f o r m a t D a t e ( d r i v e . c r e a t e d ) } < b r > < b r >
2022-01-08 15:01:13 -07:00
< b > Vehicle : < / b > $ { v e h i c l e } < b r >
< b > Openpilot Version : < / b > $ { v e r s i o n } < b r > < b r >
2022-03-24 15:03:19 -06:00
< b > Git Remote : < / b > $ { g i t R e m o t e } < b r >
< b > Git Branch : < / b > $ { g i t B r a n c h } < b r >
< b > Git Commit : < / b > $ { g i t C o m m i t } < b r > < b r >
2022-01-08 15:01:13 -07:00
< b > Num Segments : < / b > $ { d r i v e . m a x _ s e g m e n t + 1 } < b r >
< b > Storage : < /b> ${Math.round(drive.filesize / 1024 ) } MiB < br >
2022-01-09 16:19:00 -07:00
< b > Duration : < / b > $ { h e l p e r C o n t r o l l e r . f o r m a t D u r a t i o n ( d r i v e . d u r a t i o n ) } < b r >
2022-01-08 15:01:13 -07:00
< b > Distance : < /b> ${Math.round(drive.distance_meters / 1000 ) } km < br >
< b > Is Preserved : < / b > $ { d r i v e . i s _ p r e s e r v e d } < b r >
< b > Upload Complete : < / b > $ { d r i v e . u p l o a d _ c o m p l e t e } < b r >
< b > Processed : < / b > $ { d r i v e . i s _ p r o c e s s e d } < b r >
< br >
< b > Car Parameters : < / b >
< a id = "show-button" href = "#" onclick = "
document . getElementById ( 'hide-button' ) . style . display = 'inline' ;
document . getElementById ( 'show-button' ) . style . display = 'none' ;
2022-03-20 17:59:37 -06:00
document . getElementById ( 'car-parameter-div' ) . style . display = 'block' ;
2022-01-08 15:01:13 -07:00
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' ;
2022-03-20 17:59:37 -06:00
document . getElementById ( 'car-parameter-div' ) . style . display = 'none' ;
2022-01-08 15:01:13 -07:00
return false ; " > Hide < / a >
2022-03-20 17:59:37 -06:00
2022-01-08 19:23:14 -07:00
< br > < pre id = "car-parameter-div" style = "display: none; font-size: 0.8em" > $ { carParams } < / p r e >
2022-01-08 15:01:13 -07:00
< br >
< b > Preview < span id = "current_preview_segment" > < / s p a n > : < / b >
$ { cabanaUrl ? `
2021-06-09 17:17:37 -06:00
< video id = "drive_preview" class = "video-js vjs-default-skin" controls width = "480" height = "386" >
2022-03-24 17:36:29 -06:00
< source src = "${driveUrl}qcamera.m3u8" type = 'application/x-mpegURL' >
2021-06-09 17:17:37 -06:00
< / v i d e o >
< script >
2022-01-08 15:01:13 -07:00
const player = videojs ( 'drive_preview' , {
"controls" : true ,
"autoplay" : false ,
"preload" : "auto" ,
2021-06-09 17:17:37 -06:00
"controlBar" : {
"remainingTimeDisplay" : false
}
2022-01-08 15:01:13 -07:00
} ) ;
2021-06-09 17:17:37 -06:00
player . on ( 'timeupdate' , function ( ) {
2022-01-08 15:01:13 -07:00
const segment = get _current _segment _info ( this ) ;
2021-06-09 17:17:37 -06:00
document . getElementById ( 'current_preview_segment' ) . textContent = '(Segment: ' + segment [ 0 ] + ' | ' + segment [ 1 ] + '% - Timestamp: ' + segment [ 2 ] + ')' ;
} ) ;
2022-01-08 15:01:13 -07:00
2021-06-09 17:17:37 -06:00
function get _current _segment _info ( obj , old _segment = null ) {
2022-01-08 15:01:13 -07:00
const target _media = obj . tech ( ) . vhs . playlists . media ( ) ;
2021-06-09 17:17:37 -06:00
if ( ! target _media ) {
2022-01-08 15:01:13 -07:00
return [ 0 , 0 , 0 ] ;
2021-06-09 17:17:37 -06:00
}
2022-01-08 15:01:13 -07:00
let snapshot _time = obj . currentTime ( ) ;
let segment ;
let segment _time ;
for ( let i = 0 , l = target _media . segments . length ; i < l ; i ++ ) {
2021-06-09 17:17:37 -06:00
if ( snapshot _time < target _media . segments [ i ] . end ) {
segment = target _media . segments [ i ] ;
break ;
}
}
2022-01-08 15:01:13 -07:00
2021-06-09 17:17:37 -06:00
if ( segment ) {
segment _time = Math . max ( 0 , snapshot _time - ( segment . end - segment . duration ) ) ;
} else {
segment = target _media . segments [ 0 ] ;
segment _time = 0 ;
}
if ( segment ) {
2022-01-08 15:01:13 -07:00
const uri _arr = segment . uri . split ( "/" ) ;
2021-06-09 17:17:37 -06:00
return [ uri _arr [ uri _arr . length - 2 ] , Math . round ( 100 / segment . duration * segment _time ) , Math . round ( snapshot _time ) ] ;
}
return [ 0 , 0 , Math . round ( snapshot _time ) ] ;
}
< / s c r i p t >
2022-01-08 15:01:13 -07:00
` : '(available after processing)'}
< br >
$ { cabanaUrl ? ` <a href=" ${ cabanaUrl } " target=_blank><b>View Drive in CABANA</b></a> ` : 'View Drive in CABANA' }
< br > < br >
< b > Files : < / b > < b r >
< table border = 1 cellpadding = 2 cellspacing = 2 >
< tr > < th > segment < / t h > < t h > q c a m e r a < / t h > < t h > q l o g < / t h > < t h > f c a m e r a < / t h > < t h > r l o g < / t h > < t h > d c a m e r a < / t h > < t h > p r o c e s s e d < / t h > < t h > s t a l l e d < / t h > < / t r > ` ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
const directoryTree = dirTree ( ` ${ process . env . STORAGE _PATH + dongleId } / ${ dongleIdHash } / ${ driveIdentifierHash } / ${ driveIdentifier } ` ) ;
2022-01-08 15:01:13 -07:00
const directorySegments = { } ;
2022-03-24 15:03:19 -06:00
await Promise . all ( directoryTree . children . map ( async ( directory ) => {
2022-01-07 18:35:55 -07:00
// skip any non-directory entries (for example m3u8 file in the drive directory)
2022-03-24 15:03:19 -06:00
if ( directory . type !== 'directory' ) return ;
const segment = directory . name ;
let fcamera = '--' ;
let dcamera = '--' ;
let qcamera = '--' ;
let qlog = '--' ;
let rlog = '--' ;
directory . children . forEach ( ( file ) => {
2022-03-24 17:36:29 -06:00
if ( file . name === 'fcamera.hevc' ) fcamera = ` <a target="_blank" href=" ${ driveUrl } ${ segment } / ${ file . name } "> ${ file . name } </a> ` ;
else if ( file . name === 'dcamera.hevc' ) dcamera = ` <a target="_blank" href=" ${ driveUrl } ${ segment } / ${ file . name } "> ${ file . name } </a> ` ;
else if ( file . name === 'qcamera.ts' ) qcamera = ` <a target="_blank" href=" ${ driveUrl } ${ segment } / ${ file . name } "> ${ file . name } </a> ` ;
else if ( file . name === 'qlog.bz2' ) qlog = ` <a target="_blank" href=" ${ driveUrl } ${ segment } / ${ file . name } "> ${ file . name } </a> ` ;
else if ( file . name === 'rlog.bz2' ) rlog = ` <a target="_blank" href=" ${ driveUrl } ${ segment } / ${ file . name } "> ${ file . name } </a> ` ;
2022-03-24 15:21:08 -06:00
} ) ;
2021-05-21 16:17:11 -06:00
2022-03-22 07:03:17 -06:00
let isProcessed = '?' ;
let isStalled = '?' ;
2022-01-07 18:35:55 -07:00
2022-03-24 15:21:08 -06:00
const segmentId = parseInt ( segment , 10 ) ;
const driveSegment = await deviceController . getDriveSegment ( driveIdentifier , segmentId ) ;
2022-03-24 15:03:19 -06:00
if ( driveSegment ) {
isProcessed = driveSegment . is _processed ;
isStalled = driveSegment . is _stalled ;
2022-01-07 18:35:55 -07:00
}
2022-03-24 15:03:19 -06:00
directorySegments [ ` seg- ${ segment } ` ] = ` <tr><td> ${ segment } </td><td> ${ qcamera } </td><td> ${ qlog } </td><td> ${ fcamera } </td><td> ${ rlog } </td><td> ${ dcamera } </td><td> ${ isProcessed } </td><td> ${ isStalled } </td></tr> ` ;
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
for ( let i = 0 ; i <= drive . max _segment ; i ++ ) {
2022-03-24 15:21:08 -06:00
response += directorySegments [ ` seg- ${ i } ` ] || ` <tr><td> ${ i } </td><td>--</td><td>--</td><td>--</td><td>--</td><td>--</td><td>?</td><td>?</td></tr> ` ;
2022-03-24 15:03:19 -06:00
}
2022-01-16 10:32:37 -07:00
2022-03-24 15:03:19 -06:00
response += ` </table>
2022-01-16 10:32:37 -07:00
< br > < br >
< hr / >
< a href = "/useradmin/signout" > Sign Out < / a > < / b o d y > < / h t m l > ` ;
2022-03-24 15:03:19 -06:00
return res . status ( 200 ) . send ( response ) ;
2022-03-24 15:21:08 -06:00
} ) ) ;
2021-05-21 16:17:11 -06:00
2022-03-24 15:03:19 -06:00
// TODO: move to user admin api?
router . get ( '/drive/:dongleId/:driveIdentifier/:action' , requireAuthenticated , runAsyncWrapper ( async ( req , res ) => {
const {
dongleId ,
driveIdentifier ,
} = req . params ;
2022-03-24 17:34:13 -06:00
const drive = await deviceController . getDriveFromIdentifier ( dongleId , driveIdentifier ) ;
2022-03-24 15:03:19 -06:00
if ( ! drive ) {
return res . status ( 404 ) . send ( 'Not Found.' ) ;
}
const { account _id : accountId } = drive ;
if ( accountId !== req . account . id ) {
return res . status ( 403 ) . send ( 'Forbidden.' ) ;
}
const { action } = req . params ;
if ( action === 'delete' ) {
await deviceController . updateOrCreateDrive ( dongleId , driveIdentifier , {
is _deleted : true ,
} ) ;
} else if ( action === 'preserve' ) {
await deviceController . updateOrCreateDrive ( dongleId , driveIdentifier , {
is _preserved : true ,
} ) ;
}
return res . redirect ( ` /useradmin/device/ ${ dongleId } ` ) ;
} ) ) ;
2022-01-12 08:02:30 -07:00
export default router ;