Merge pull request #3 from AdamSBlack/main
Continuation, routes now in /routes and /controllers more fleshed out + testspull/4/head
commit
b9cf8e26d9
|
@ -6,3 +6,7 @@ package-lock.json
|
|||
database.sqlite
|
||||
config.js
|
||||
.vscode
|
||||
.idea
|
||||
database.sqlite
|
||||
config.js
|
||||
test/.devKeys
|
|
@ -1,19 +1,45 @@
|
|||
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
let models;
|
||||
let logger;
|
||||
|
||||
|
||||
async function validateJWT(token, key) {
|
||||
try {
|
||||
return jwt.verify(token.replace("JWT ", ""), key, {algorithms: ['RS256']});
|
||||
} catch (exception) {
|
||||
// TODO add logger to authentication controller
|
||||
//logger.error(exception);
|
||||
//logger.warn(exception)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
validateJWT: validateJWT
|
||||
|
||||
async function getAuthenticatedAccount(req, res) {
|
||||
const sessionCookie = (req.signedCookies !== undefined ? req.signedCookies.session : null);
|
||||
if (!sessionCookie || sessionCookie.expires <= Date.now()) { return null; }
|
||||
const email = sessionCookie.account.trim().toLowerCase();
|
||||
|
||||
|
||||
|
||||
// TODO stop storing emails in the cookie
|
||||
const account = await models.users.getAccountFromEmail(email)
|
||||
// Don't really care about this returning.
|
||||
models.users.userPing(account.email);
|
||||
|
||||
if (!account || account.banned) {
|
||||
return res ? res.clearCookie('session') : logger.warn(`getAuthenticatedAccount unable to clear banned user (${account.email}) cookie, res not passed`);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
|
||||
module.exports = (_models, _logger) => {
|
||||
models = _models;
|
||||
logger = _logger;
|
||||
|
||||
return {
|
||||
validateJWT: validateJWT,
|
||||
getAuthenticatedAccount: getAuthenticatedAccount
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
let models;
|
||||
let logger;
|
||||
|
||||
|
||||
function formatDuration(durationSeconds) {
|
||||
durationSeconds = Math.round(durationSeconds);
|
||||
const secs = durationSeconds % 60;
|
||||
let mins = Math.floor(durationSeconds / 60);
|
||||
const hours = Math.floor(mins / 60);
|
||||
mins = mins % 60;
|
||||
|
||||
let response = '';
|
||||
if (hours > 0) response += hours + 'h ';
|
||||
if (hours > 0 || mins > 0) response += mins + 'm ';
|
||||
response += secs + 's';
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
function simpleStringify(object) {
|
||||
let simpleObject = {};
|
||||
for (var prop in object) {
|
||||
if (!object.hasOwnProperty(prop)) {
|
||||
continue;
|
||||
}
|
||||
if (typeof (object[prop]) == 'object') {
|
||||
continue;
|
||||
}
|
||||
if (typeof (object[prop]) == 'function') {
|
||||
continue;
|
||||
}
|
||||
simpleObject[prop] = object[prop];
|
||||
}
|
||||
return JSON.stringify(simpleObject); // returns cleaned up JSON
|
||||
}
|
||||
|
||||
|
||||
function formatDate(timestampMs) {
|
||||
return new Date(timestampMs).toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = (_models, _logger) => {
|
||||
models = _models;
|
||||
logger = _logger;
|
||||
|
||||
return {
|
||||
formatDuration, simpleStringify, formatDate
|
||||
}
|
||||
}
|
|
@ -1,6 +1,13 @@
|
|||
const config = require('./../config');
|
||||
|
||||
|
||||
module.exports = async (models, logger) => {
|
||||
|
||||
module.exports = {
|
||||
authenticationController: require('./authentication')
|
||||
}
|
||||
|
||||
return {
|
||||
authentication: require('./authentication')(models, logger),
|
||||
helpers: require('./helpers')(models, logger),
|
||||
storage: require('./storage')(models, logger)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
const config = require('./../config');
|
||||
const path = require('path');
|
||||
const fs = require('fs')
|
||||
|
||||
let models;
|
||||
let logger;
|
||||
|
||||
let totalStorageUsed;
|
||||
|
||||
function initializeStorage() {
|
||||
var verifiedPath = mkDirByPathSync(config.storagePath, {isRelativeToScript: (config.storagePath.indexOf("/") === 0 ? false : true)});
|
||||
if (verifiedPath != null)
|
||||
logger.info("Verified storage path " + verifiedPath);
|
||||
else {
|
||||
logger.error("Unable to verify storage path '" + config.storagePath + "', check filesystem / permissions");
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
function mkDirByPathSync(targetDir, {isRelativeToScript = false} = {}) {
|
||||
const sep = path.sep;
|
||||
const initDir = path.isAbsolute(targetDir) ? sep : '';
|
||||
|
||||
const baseDir = isRelativeToScript ? __basedir : '.';
|
||||
|
||||
return targetDir.split(sep).reduce((parentDir, childDir) => {
|
||||
const curDir = path.resolve(baseDir, parentDir, childDir);
|
||||
try {
|
||||
fs.mkdirSync(curDir);
|
||||
} catch (err) {
|
||||
//console.debug(err);
|
||||
if (err.code === 'EEXIST') { // curDir already exists!
|
||||
return curDir;
|
||||
}
|
||||
|
||||
// To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
|
||||
if (err.code === 'ENOENT') { // Throw the original parentDir error on curDir `ENOENT` failure.
|
||||
logger.error(`EACCES: permission denied, mkdir '${parentDir}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1;
|
||||
if (!caughtErr || (caughtErr && curDir === path.resolve(targetDir))) {
|
||||
logger.error("'EACCES', 'EPERM', 'EISDIR' during mkdir");
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return curDir;
|
||||
}, initDir);
|
||||
}
|
||||
|
||||
|
||||
function writeFileSync(path, buffer, permission) {
|
||||
let fileDescriptor;
|
||||
try {
|
||||
fileDescriptor = fs.openSync(path, 'w', permission);
|
||||
} catch (e) {
|
||||
fs.chmodSync(path, permission);
|
||||
fileDescriptor = fs.openSync(path, 'w', permission);
|
||||
}
|
||||
|
||||
if (fileDescriptor) {
|
||||
fs.writeSync(fileDescriptor, buffer, 0, buffer.length, 0);
|
||||
fs.closeSync(fileDescriptor);
|
||||
logger.info("writeFileSync wiriting to '" + path + "' successful");
|
||||
return true;
|
||||
}
|
||||
logger.error("writeFileSync writing to '" + path + "' failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
function moveUploadedFile(buffer, directory, filename) {
|
||||
logger.info(`moveUploadedFile called with ${filename} -> ${directory}'`);
|
||||
|
||||
if (directory.indexOf("..") >= 0 || filename.indexOf("..") >= 0) {
|
||||
logger.error("moveUploadedFile failed, .. in directory or filename");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.storagePath.lastIndexOf("/") !== config.storagePath.length - 1)
|
||||
directory = '/' + directory;
|
||||
if (directory.lastIndexOf("/") !== directory.length - 1)
|
||||
directory = directory + '/';
|
||||
|
||||
const finalPath = mkDirByPathSync(config.storagePath + directory, {isRelativeToScript: (config.storagePath.indexOf("/") === 0 ? false : true)});
|
||||
if (finalPath && finalPath.length > 0) {
|
||||
if (writeFileSync(finalPath + "/" + filename, buffer, 0o660)) {
|
||||
logger.info("moveUploadedFile successfully written '" + (finalPath + "/" + filename) + "'");
|
||||
return finalPath + "/" + filename;
|
||||
}
|
||||
logger.error("moveUploadedFile failed to writeFileSync");
|
||||
return false;
|
||||
}
|
||||
logger.error("moveUploadedFile invalid final path, check permissions to create / write '" + (config.storagePath + directory) + "'");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
async function updateTotalStorageUsed() {
|
||||
const verifiedPath = mkDirByPathSync(config.storagePath, {isRelativeToScript: (config.storagePath.indexOf("/") === 0 ? false : true)});
|
||||
if (verifiedPath !== null) {
|
||||
try {
|
||||
totalStorageUsed = execSync("du -hs " + verifiedPath + " | awk -F'\t' '{print $1;}'").toString();
|
||||
} catch (exception) {
|
||||
totalStorageUsed = "Unsupported Platform";
|
||||
logger.debug(`Unable to calculate storage used, only supported on systems with 'du' available`)
|
||||
}
|
||||
}
|
||||
setTimeout(function () {
|
||||
updateTotalStorageUsed();
|
||||
}, 120000); // update the used storage each 120 seconds
|
||||
}
|
||||
|
||||
async function getTotalStorageUsed() {
|
||||
return totalStorageUsed;
|
||||
}
|
||||
|
||||
|
||||
module.exports = (_models, _logger) => {
|
||||
models = _models;
|
||||
logger = _logger;
|
||||
|
||||
return {
|
||||
initializeStorage, mkDirByPathSync, writeFileSync, moveUploadedFile, updateTotalStorageUsed, getTotalStorageUsed
|
||||
}
|
||||
}
|
|
@ -2,11 +2,7 @@ const sqlite3 = require('sqlite3')
|
|||
const {open} = require('sqlite')
|
||||
const config = require('./../config');
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = async (logger) => {
|
||||
let db;
|
||||
async function validateDatabase(db, logger) {
|
||||
try {
|
||||
db = await open({
|
||||
filename: config.databaseFile,
|
||||
|
@ -22,12 +18,38 @@ module.exports = async (logger) => {
|
|||
logger.error(exception);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = async (logger) => {
|
||||
let db;
|
||||
|
||||
try {
|
||||
db = await open({
|
||||
filename: config.databaseFile,
|
||||
driver: sqlite3.Database,
|
||||
mode: sqlite3.OPEN_READWRITE
|
||||
});
|
||||
|
||||
} catch (exception) {
|
||||
logger.error(exception);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// I'm not sure we _really_ need to wait for this, since it'll exit the application if it's invalid anyway.
|
||||
|
||||
await validateDatabase(db, logger);
|
||||
|
||||
|
||||
return {
|
||||
db,
|
||||
models: {
|
||||
drivesModel: require('./drives')(db)
|
||||
drivesModel: require('./drives')(db),
|
||||
users: require('./users')(db),
|
||||
|
||||
// TODO remove access to DB queries from non models
|
||||
__db: db // to be removed when db queries are removed from outside models.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
let db;
|
||||
|
||||
async function userPing(email) {
|
||||
return await db.run('UPDATE accounts SET last_ping = ? WHERE email = ?', Date.now(), email);
|
||||
}
|
||||
|
||||
async function getAccountFromEmail(email) {
|
||||
return await db.get('SELECT * FROM accounts WHERE LOWER(email) = ?', email);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = (_db) => {
|
||||
db = _db;
|
||||
|
||||
return {
|
||||
userPing,
|
||||
getAccountFromEmail
|
||||
}
|
||||
}
|
|
@ -4,13 +4,15 @@
|
|||
"description": "replacement for comma.ai backend and useradmin dashboard. can be combined with a modified cabana instance.",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "mocha",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@commaai/log_reader": "^0.8.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-http": "^4.3.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
|
@ -25,10 +27,12 @@
|
|||
"htmlspecialchars": "^1.0.5",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"log4js": "^6.3.0",
|
||||
"mocha": "^8.4.0",
|
||||
"multer": "^1.4.2",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"sendmail": "^1.6.1",
|
||||
"sqlite": "^4.0.22",
|
||||
"sqlite3": "^5.0.2"
|
||||
"sqlite3": "^5.0.2",
|
||||
"supertest": "^6.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,328 @@
|
|||
const router = require('express').Router();
|
||||
const bodyParser = require('body-parser');
|
||||
const crypto = require('crypto');
|
||||
const config = require('./../config');
|
||||
|
||||
|
||||
function runAsyncWrapper(callback) {
|
||||
return function (req, res, next) {
|
||||
callback(req, res, next)
|
||||
.catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
let models;
|
||||
let controllers;
|
||||
let logger;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// DRIVE & BOOT/CRASH LOG FILE UPLOAD HANDLING
|
||||
router.put('/backend/post_upload', bodyParser.raw({
|
||||
inflate: true,
|
||||
limit: '100000kb',
|
||||
type: '*/*'
|
||||
}), runAsyncWrapper(async (req, res) => {
|
||||
var buf = new Buffer(req.body.toString('binary'), 'binary');
|
||||
logger.info("HTTP.PUT /backend/post_upload for dongle " + req.query.dongleId + " with body length: " + buf.length);
|
||||
|
||||
var dongleId = req.query.dongleId;
|
||||
var ts = req.query.ts;
|
||||
|
||||
if (req.query.file.indexOf("boot") != 0 && req.query.file.indexOf("crash") != 0) { // drive file upload
|
||||
var filename = req.query.file;
|
||||
var directory = req.query.dir;
|
||||
var token = crypto.createHmac('sha256', config.applicationSalt).update(dongleId + filename + directory + ts).digest('hex');
|
||||
|
||||
logger.info("HTTP.PUT /backend/post_upload DRIVE upload with filename: " + filename + ", directory: " + directory + ", token: " + req.query.token);
|
||||
|
||||
if (token !== req.query.token) {
|
||||
logger.error("HTTP.PUT /backend/post_upload token mismatch (" + token + " vs " + req.query.token + ")");
|
||||
res.status(400);
|
||||
res.send('Malformed request');
|
||||
|
||||
} else {
|
||||
logger.info("HTTP.PUT /backend/post_upload permissions checked, calling moveUploadedFile");
|
||||
var moveResult = controllers.storage.moveUploadedFile(buf, directory, filename);
|
||||
if (moveResult === false) {
|
||||
logger.error("HTTP.PUT /backend/post_upload moveUploadedFile failed");
|
||||
res.status(500);
|
||||
res.send('Internal Server Error');
|
||||
} else {
|
||||
logger.info("HTTP.PUT /backend/post_upload succesfully uploaded to " + moveResult);
|
||||
res.status(200);
|
||||
res.json(['OK']);
|
||||
}
|
||||
}
|
||||
} else { // boot or crash upload
|
||||
var filename = req.query.file;
|
||||
var token = crypto.createHmac('sha256', config.applicationSalt).update(dongleId + filename + ts).digest('hex');
|
||||
var directory = req.query.dir;
|
||||
|
||||
logger.info("HTTP.PUT /backend/post_upload BOOT or CRASH upload with filename: " + filename + ", token: " + req.query.token);
|
||||
if (token !== req.query.token) {
|
||||
logger.error("HTTP.PUT /backend/post_upload token mismatch (" + token + " vs " + req.query.token + ")");
|
||||
res.status(400);
|
||||
res.send('Malformed request');
|
||||
|
||||
} else {
|
||||
logger.info("HTTP.PUT /backend/post_upload permissions checked, calling moveUploadedFile");
|
||||
var moveResult = controllers.storage.moveUploadedFile(buf, directory, filename);
|
||||
if (moveResult === false) {
|
||||
logger.error("HTTP.PUT /backend/post_upload moveUploadedFile failed");
|
||||
res.status(500);
|
||||
res.send('Internal Server Error');
|
||||
} else {
|
||||
logger.info("HTTP.PUT /backend/post_upload succesfully uploaded to " + moveResult);
|
||||
res.status(200);
|
||||
res.json(['OK']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
// DRIVE & BOOT/CRASH LOG FILE UPLOAD URL REQUEST
|
||||
router.get('/v1.3/:dongleId/upload_url/', runAsyncWrapper(async (req, res) => {
|
||||
var path = req.query.path;
|
||||
const dongleId = req.params.dongleId;
|
||||
const auth = req.params.authorization;
|
||||
logger.info("HTTP.UPLOAD_URL called for " + req.params.dongleId + " and file " + path + ": " + JSON.stringify(req.headers));
|
||||
|
||||
|
||||
const device = await models.drivesModel.getDevice(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)
|
||||
}
|
||||
|
||||
let decoded = device.public_key ? await controllers.authentication.validateJWT(req.headers.authorization, device.public_key) : null;
|
||||
|
||||
if ((decoded == undefined || decoded.identity !== req.params.dongleId)) {
|
||||
logger.info(`HTTP.UPLOAD_URL JWT authorization failed, token: ${auth} device: ${JSON.stringify(device)}, decoded: ${JSON.stringify(decoded)}`);
|
||||
return res.send('Unauthorized.').status(400)
|
||||
}
|
||||
|
||||
await models.drivesModel.deviceCheckIn(dongleId)
|
||||
|
||||
let responseUrl = null;
|
||||
const ts = Date.now(); // we use this to make sure old URLs cannot be reused (timeout after 60min)
|
||||
|
||||
const dongleIdHash = crypto.createHmac('sha256', config.applicationSalt).update(dongleId).digest('hex');
|
||||
|
||||
// boot log upload
|
||||
|
||||
if (path.indexOf("boot/") === 0 || path.indexOf("crash/") === 0) {
|
||||
let filename = path.replace("/", "-");
|
||||
const token = crypto.createHmac('sha256', config.applicationSalt).update(dongleId + filename + ts).digest('hex');
|
||||
|
||||
// TODO, allow multiple types
|
||||
let uplaodType = path.indexOf("boot/") === 0 ? 'boot' : 'crash';
|
||||
|
||||
// "boot-2021-04-12--01-45-30.bz" for example
|
||||
const directory = `${dongleId}/${dongleIdHash}/${uplaodType}`;
|
||||
responseUrl = `${config.baseUploadUrl}?file=${filename}&dir=${directory}&dongleId=${dongleId}&ts=${ts}&token=${token}`;
|
||||
logger.info(`HTTP.UPLOAD_URL matched '${uplaodType}' file upload, constructed responseUrl: ${responseUrl}`);
|
||||
} else {
|
||||
// "2021-04-12--01-44-25--0/qlog.bz2" for example
|
||||
const subdirPosition = path.split("--", 2).join("--").length;
|
||||
const filenamePosition = path.indexOf("/");
|
||||
if (subdirPosition > 0 && filenamePosition > subdirPosition) {
|
||||
const driveName = `${path.split("--")[0]}--${path.split("--")[1]}`
|
||||
const segment = parseInt(path.split("--")[2].substr(0, path.split("--")[2].indexOf("/")));
|
||||
let directory = `${path.split("--")[0]}--${path.split("--")[1]}/${segment}`;
|
||||
const filename = path.split("/")[1];
|
||||
|
||||
let validRequest = false;
|
||||
|
||||
if ((filename === 'fcamera.hevc' || filename === 'qcamera.ts' || filename === 'dcamera.hevc' || filename === 'rlog.bz2' || filename === 'qlog.bz2') &&
|
||||
(!isNaN(segment) || (segment > 0 && segment < 1000))) {
|
||||
validRequest = true;
|
||||
}
|
||||
|
||||
if (!validRequest) {
|
||||
logger.error(`HTTP.UPLOAD_URL invalid filename (${filename}) or invalid segment (${segment}), responding with HTTP 400`);
|
||||
return res.send('Malformed Request.').status(400)
|
||||
}
|
||||
|
||||
const driveIdentifierHash = crypto.createHmac('sha256', config.applicationSalt).update(driveName).digest('hex');
|
||||
|
||||
directory = `${dongleId}/${dongleIdHash}/${driveIdentifierHash}/${directory}`;
|
||||
|
||||
const token = crypto.createHmac('sha256', config.applicationSalt).update(dongleId + filename + directory + ts).digest('hex');
|
||||
responseUrl = `${config.baseUploadUrl}?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 models.__db.get('SELECT * FROM drives WHERE identifier = ? AND dongle_id = ?', driveName, dongleId);
|
||||
|
||||
if (drive == null) {
|
||||
// create a new drive
|
||||
const timeSplit = driveName.split('--');
|
||||
const timeString = timeSplit[0] + ' ' + timeSplit[1].replace(/-/g, ':');
|
||||
|
||||
const driveResult = await models.__db.run(
|
||||
'INSERT INTO drives (identifier, dongle_id, max_segment, duration, distance_meters, filesize, upload_complete, is_processed, drive_date, created, last_upload, is_preserved, is_deleted, is_physically_removed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
driveName, dongleId, segment, 0, 0, 0, false, false, Date.parse(timeString), Date.now(), Date.now(), false, false, false);
|
||||
|
||||
|
||||
const driveSegmentResult = await models.__db.run(
|
||||
'INSERT INTO drive_segments (segment_id, drive_identifier, dongle_id, duration, distance_meters, upload_complete, is_processed, is_stalled, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
segment, driveName, dongleId, 0, 0, false, false, false, Date.now());
|
||||
|
||||
logger.info("HTTP.UPLOAD_URL created new drive #" + JSON.stringify(driveResult.lastID));
|
||||
} else {
|
||||
const driveResult = await models.__db.run(
|
||||
'UPDATE drives SET last_upload = ?, max_segment = ?, upload_complete = ?, is_processed = ? WHERE identifier = ? AND dongle_id = ?',
|
||||
Date.now(), Math.max(drive.max_segment, segment), false, false, driveName, dongleId
|
||||
);
|
||||
|
||||
const drive_segment = await models.__db.get('SELECT * FROM drive_segments WHERE drive_identifier = ? AND dongle_id = ? AND segment_id = ?', driveName, dongleId, segment);
|
||||
|
||||
if (drive_segment == null) {
|
||||
const driveSegmentResult = await models.__db.run(
|
||||
'INSERT INTO drive_segments (segment_id, drive_identifier, dongle_id, duration, distance_meters, upload_complete, is_processed, is_stalled, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
segment, driveName, dongleId, 0, 0, false, false, false, Date.now()
|
||||
);
|
||||
} else {
|
||||
const driveSegmentResult = await models.__db.run(
|
||||
'UPDATE drive_segments SET upload_complete = ?, is_stalled = ? WHERE drive_identifier = ? AND dongle_id = ? AND segment_id = ?',
|
||||
false, false, driveName, dongleId, segment
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("HTTP.UPLOAD_URL updated existing drive: " + JSON.stringify(drive));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (responseUrl != null) {
|
||||
res.status(200);
|
||||
res.json({'url': responseUrl, 'headers': {'Content-Type': 'application/octet-stream'}});
|
||||
} else {
|
||||
logger.error("HTTP.UPLOAD_URL unable to match request, responding with HTTP 400");
|
||||
res.status(400);
|
||||
res.send('Malformed Request.');
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
// DEVICE REGISTRATION OR RE-ACTIVATION
|
||||
router.post('/v2/pilotauth/', bodyParser.urlencoded({extended: true}), async (req, res) => {
|
||||
var imei1 = req.query.imei;
|
||||
var serial = req.query.serial;
|
||||
var public_key = req.query.public_key;
|
||||
var register_token = req.query.register_token;
|
||||
|
||||
if (imei1 == null || imei1.length < 5 || serial == null || serial.length < 5 || public_key == null || public_key.length < 5 || register_token == null || register_token.length < 5) {
|
||||
logger.error(`HTTP.V2.PILOTAUTH a required parameter is missing or empty ${JSON.stringify(req.query)}`);
|
||||
res.status(400);
|
||||
res.send('Malformed Request.');
|
||||
return;
|
||||
}
|
||||
var decoded = await controllers.authentication.validateJWT(req.query.register_token, public_key);
|
||||
|
||||
|
||||
if (decoded == null || decoded.register == undefined) {
|
||||
logger.error("HTTP.V2.PILOTAUTH JWT token is invalid (" + JSON.stringify(decoded) + ")");
|
||||
res.status(400);
|
||||
res.send('Malformed Request.');
|
||||
return;
|
||||
}
|
||||
|
||||
const device = await models.__db.get('SELECT * FROM devices WHERE imei = ? AND serial = ?', imei1, serial);
|
||||
if (device == null) {
|
||||
logger.info("HTTP.V2.PILOTAUTH REGISTERING NEW DEVICE (" + imei1 + ", " + serial + ")");
|
||||
while (true) {
|
||||
var dongleId = crypto.randomBytes(4).toString('hex');
|
||||
const isDongleIdTaken = await models.__db.get('SELECT * FROM devices WHERE imei = ? AND serial = ?', imei1, serial);
|
||||
if (isDongleIdTaken == null) {
|
||||
const resultingDevice = await models.__db.run(
|
||||
'INSERT INTO devices (dongle_id, account_id, imei, serial, device_type, public_key, created, last_ping, storage_used) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
dongleId, 0, imei1, serial, 'freon', public_key, Date.now(), Date.now(), 0);
|
||||
|
||||
const device = await models.__db.get('SELECT * FROM devices WHERE dongle_id = ?', dongleId);
|
||||
|
||||
logger.info("HTTP.V2.PILOTAUTH REGISTERED NEW DEVICE: " + JSON.stringify(device));
|
||||
res.status(200);
|
||||
res.json({dongle_id: device.dongle_id});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const result = await models.__db.run(
|
||||
'UPDATE devices SET last_ping = ?, public_key = ? WHERE dongle_id = ?',
|
||||
Date.now(), public_key, device.dongle_id
|
||||
);
|
||||
|
||||
logger.info("HTTP.V2.PILOTAUTH REACTIVATING KNOWN DEVICE (" + imei1 + ", " + serial + ") with dongle_id " + device.dongle_id + "");
|
||||
res.status(200);
|
||||
res.json({dongle_id: device.dongle_id});
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// RETRIEVES DATASET FOR OUR MODIFIED CABANA - THIS RESPONSE IS USED TO FAKE A DEMO ROUTE
|
||||
router.get('/useradmin/cabana_drive/:extendedRouteIdentifier', runAsyncWrapper(async (req, res) => {
|
||||
|
||||
var params = req.params.extendedRouteIdentifier.split('|');
|
||||
var dongleId = params[0];
|
||||
var dongleIdHashReq = params[1];
|
||||
var driveIdentifier = params[2];
|
||||
var driveIdentifierHashReq = params[3];
|
||||
|
||||
const drive = await models.__db.get('SELECT * FROM drives WHERE identifier = ? AND dongle_id = ?', driveIdentifier, dongleId);
|
||||
|
||||
if (!drive) {
|
||||
res.status(200);
|
||||
res.json({'status': 'drive not found'});
|
||||
return;
|
||||
}
|
||||
|
||||
var dongleIdHash = crypto.createHmac('sha256', config.applicationSalt).update(drive.dongle_id).digest('hex');
|
||||
var driveIdentifierHash = crypto.createHmac('sha256', config.applicationSalt).update(drive.identifier).digest('hex');
|
||||
var driveUrl = config.baseDriveDownloadUrl + drive.dongle_id + "/" + dongleIdHash + "/" + driveIdentifierHash + "/" + drive.identifier;
|
||||
|
||||
if (dongleIdHash != dongleIdHashReq || driveIdentifierHash != driveIdentifierHashReq) {
|
||||
res.status(200);
|
||||
res.json({'status': 'hashes not matching'});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!drive.is_processed) {
|
||||
res.status(200);
|
||||
res.json({'status': 'drive is not processed yet'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var logUrls = [];
|
||||
|
||||
for (var i = 0; i <= drive.max_segment; i++) {
|
||||
logUrls.push(driveUrl + '/' + i + '/rlog.bz2');
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.json({
|
||||
logUrls: logUrls,
|
||||
driveUrl: driveUrl,
|
||||
name: drive.dongle_id + '|' + drive.identifier,
|
||||
driveIdentifier: drive.identifier,
|
||||
dongleId: drive.dongle_id
|
||||
});
|
||||
}))
|
||||
|
||||
|
||||
|
||||
module.exports = (_models, _controllers, _logger) => {
|
||||
models = _models;
|
||||
controllers = _controllers;
|
||||
logger = _logger;
|
||||
|
||||
return router;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
|
||||
|
||||
|
||||
module.exports = (_models, _controllers, _logger) => {
|
||||
return {
|
||||
useradmin: require('./useradmin')(_models, _controllers, _logger),
|
||||
api: require('./api')(_models, _controllers, _logger)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,562 @@
|
|||
const router = require('express').Router();
|
||||
const bodyParser = require('body-parser');
|
||||
const crypto = require('crypto');
|
||||
const sendmail = require('sendmail')({silent: false});
|
||||
const htmlspecialchars = require('htmlspecialchars');
|
||||
const dirTree = require("directory-tree");
|
||||
const cookieParser = require('cookie-parser');
|
||||
const config = require('./../config');
|
||||
|
||||
// TODO Remove this, pending on removing all auth logic from routes
|
||||
router.use(cookieParser(config.applicationSalt))
|
||||
|
||||
|
||||
|
||||
function runAsyncWrapper(callback) {
|
||||
return function (req, res, next) {
|
||||
callback(req, res, next)
|
||||
.catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
let models;
|
||||
let controllers;
|
||||
let logger;
|
||||
|
||||
|
||||
router.post('/useradmin/auth', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => {
|
||||
const account = await models.__db.get('SELECT * FROM accounts WHERE email = ? AND password = ?', req.body.email, crypto.createHash('sha256').update(req.body.password + config.applicationSalt).digest('hex'));
|
||||
|
||||
if (!account || account.banned) {
|
||||
res.status(200);
|
||||
res.redirect('/useradmin?status=' + encodeURIComponent('Invalid credentials or banned account'));
|
||||
return;
|
||||
}
|
||||
res.cookie('session', {account: account.email, expires: Date.now() + 1000 * 3600 * 24 * 365}, {signed: true});
|
||||
res.redirect('/useradmin/overview');
|
||||
}))
|
||||
|
||||
|
||||
router.get('/useradmin/signout', runAsyncWrapper(async (req, res) => {
|
||||
res.clearCookie('session');
|
||||
res.redirect('/useradmin');
|
||||
}))
|
||||
|
||||
|
||||
router.get('/useradmin', runAsyncWrapper(async (req, res) => {
|
||||
const account = await controllers.authentication.getAuthenticatedAccount(req, res);
|
||||
if (account != null) {
|
||||
res.redirect('/useradmin/overview');
|
||||
return;
|
||||
}
|
||||
|
||||
const accounts = await models.__db.get('SELECT COUNT(*) AS num FROM accounts');
|
||||
const devices = await models.__db.get('SELECT COUNT(*) AS num FROM devices');
|
||||
const drives = await models.__db.get('SELECT COUNT(*) AS num FROM drives');
|
||||
|
||||
res.status(200);
|
||||
res.send('<html style="font-family: monospace"><h2>Welcome To The RetroPilot Server Dashboard!</h2>' +
|
||||
`<br><br>
|
||||
<h3>Login</h3>
|
||||
` + (req.query.status !== undefined ? '<u>' + htmlspecialchars(req.query.status) + '</u><br>' : '') + `
|
||||
<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">
|
||||
</form><br><br>` + (!config.allowAccountRegistration ? '<i>User Account Registration is disabled on this Server</i>' : '<a href="/useradmin/register">Register new Account</a>') + `<br><br>` +
|
||||
'Accounts: ' + accounts.num + ' | ' +
|
||||
'Devices: ' + devices.num + ' | ' +
|
||||
'Drives: ' + drives.num + ' | ' +
|
||||
'Storage Used: ' + (await controllers.storage.getTotalStorageUsed() !== null ? await controllers.storage.getTotalStorageUsed() : '--') + '<br><br>' + config.welcomeMessage + '</html>');
|
||||
}))
|
||||
|
||||
|
||||
router.post('/useradmin/register/token', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => {
|
||||
if (!req.body.hasOwnProperty('email') || req.body.email === "") {
|
||||
res.status(400)
|
||||
res.send('Malformed Request');
|
||||
logger.warn("/useradmin/register/token - Malformed Request!")
|
||||
return;
|
||||
}
|
||||
|
||||
const email = req.body.email;
|
||||
|
||||
|
||||
if (!config.allowAccountRegistration) {
|
||||
res.send('Unauthorized.').status(401);
|
||||
return;
|
||||
}
|
||||
|
||||
const authAccount = await controllers.authentication.getAuthenticatedAccount(req, res);
|
||||
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;
|
||||
}
|
||||
|
||||
var token = crypto.createHmac('sha256', config.applicationSalt).update(email.trim()).digest('hex');
|
||||
|
||||
var infoText = '';
|
||||
|
||||
if (req.body.token === undefined) { // email entered, token request
|
||||
logger.info("USERADMIN REGISTRATION sending token to " + htmlspecialchars(email.trim()) + ": \"" + token + "\"");
|
||||
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>';
|
||||
|
||||
sendmail({
|
||||
from: 'no-reply@retropilot.com',
|
||||
to: email.trim(),
|
||||
subject: 'RetroPilot Registration Token',
|
||||
html: 'Your Email Registration Token Is: "' + token + '"',
|
||||
}, function (err, reply) {
|
||||
if (err)
|
||||
logger.error("USERADMIN REGISTRATION - failed to send registration token email " + (err && err.stack) + " " + reply);
|
||||
});
|
||||
} else { // final registration form filled
|
||||
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 or were shorter than 3 characters, please try again.<br><br>';
|
||||
} else {
|
||||
|
||||
const result = await models.__db.run(
|
||||
'INSERT INTO accounts (email, password, created, banned) VALUES (?, ?, ?, ?)',
|
||||
email,
|
||||
crypto.createHash('sha256').update(req.body.password + config.applicationSalt).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).<br><br>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.send('<html style="font-family: monospace"><h2>Welcome To The RetroPilot Server Dashboard!</h2>' +
|
||||
`
|
||||
<a href="/useradmin">< < < Back To Login</a>
|
||||
<br><br>
|
||||
<h3>Register / Finish Registration</h3>
|
||||
` + 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 != undefined ? htmlspecialchars(req.body.token.trim()) : '') + `" required><br>
|
||||
<input type="password" name="password" placeholder="Password" value="` + (req.body.password != undefined ? htmlspecialchars(req.body.password.trim()) : '') + `" required>
|
||||
<input type="password" name="password2" placeholder="Repeat Password" value="` + (req.body.password2 != undefined ? htmlspecialchars(req.body.password2.trim()) : '') + `" required>
|
||||
<input type="submit" value="Finish Registration">
|
||||
</html>`);
|
||||
}))
|
||||
|
||||
router.get('/useradmin/register', runAsyncWrapper(async (req, res) => {
|
||||
if (!config.allowAccountRegistration) {
|
||||
res.status(400);
|
||||
res.send('Unauthorized.');
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await controllers.authentication.getAuthenticatedAccount(req, res);
|
||||
if (account != null) {
|
||||
res.redirect('/useradmin/overview');
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.send('<html style="font-family: monospace"><h2>Welcome To The RetroPilot Server Dashboard!</h2>' +
|
||||
`
|
||||
<a href="/useradmin">< < < Back To Login</a>
|
||||
<br><br>
|
||||
<h3>Register / Request Email Token</h3>
|
||||
` + (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">
|
||||
</html>`);
|
||||
}))
|
||||
|
||||
|
||||
router.get('/useradmin/overview', runAsyncWrapper(async (req, res) => {
|
||||
const account = await controllers.authentication.getAuthenticatedAccount(req, res);
|
||||
if (account == null) {
|
||||
res.redirect('/useradmin?status=' + encodeURIComponent('Invalid or expired session'));
|
||||
return;
|
||||
}
|
||||
|
||||
const devices = await models.__db.all('SELECT * FROM devices WHERE account_id = ? ORDER BY dongle_id ASC', account.id)
|
||||
|
||||
var response = '<html style="font-family: monospace"><h2>Welcome To The RetroPilot Server Dashboard!</h2>' +
|
||||
|
||||
`<br><br><h3>Account Overview</h3>
|
||||
<b>Account:</b> #` + account.id + `<br>
|
||||
<b>Email:</b> ` + account.email + `<br>
|
||||
<b>Created:</b> ` + controllers.helpers.formatDate(account.created) + `<br><br>
|
||||
<b>Devices:</b><br>
|
||||
<table border=1 cellpadding=2 cellspacing=2>
|
||||
<tr><th>dongle_id</th><th>device_type</th><th>created</th><th>last_ping</th><th>storage_used</th></tr>
|
||||
`;
|
||||
|
||||
for (var i in devices) {
|
||||
response += '<tr><td><a href="/useradmin/device/' + devices[i].dongle_id + '">' + devices[i].dongle_id + '</a></td><td>' + devices[i].device_type + '</td><td>' + controllers.helpers.formatDate(devices[i].created) + '</td><td>' + controllers.helpers.formatDate(devices[i].last_ping) + '</td><td>' + devices[i].storage_used + ' MB</td></tr>';
|
||||
}
|
||||
response += `</table>
|
||||
<br>
|
||||
<hr/>
|
||||
<h3>Pair New Devices</h3>
|
||||
<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><br>
|
||||
` + (req.query.linkstatus !== undefined ? '<br><u>' + htmlspecialchars(req.query.linkstatus) + '</u><br><br>' : '') + `
|
||||
<form action="/useradmin/pair_device" method="POST">
|
||||
<input type="text" name="qr_string" placeholder="QR Code Device Token" required>
|
||||
<input type="submit" value="Pair">
|
||||
</form><br><br>
|
||||
<hr/>
|
||||
<a href="/useradmin/signout">Sign Out</a>`;
|
||||
|
||||
res.status(200);
|
||||
res.send(response);
|
||||
|
||||
}))
|
||||
|
||||
|
||||
router.get('/useradmin/unpair_device/:dongleId', runAsyncWrapper(async (req, res) => {
|
||||
const account = await controllers.authentication.getAuthenticatedAccount(req, res);
|
||||
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 result = await models.__db.run(
|
||||
'UPDATE devices SET account_id = ? WHERE dongle_id = ?',
|
||||
0,
|
||||
req.params.dongleId
|
||||
);
|
||||
|
||||
res.redirect('/useradmin/overview');
|
||||
})),
|
||||
|
||||
|
||||
router.post('/useradmin/pair_device', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => {
|
||||
const account = await controllers.authentication.getAuthenticatedAccount(req, res);
|
||||
if (account == null) {
|
||||
res.redirect('/useradmin?status=' + encodeURIComponent('Invalid or expired session'));
|
||||
return;
|
||||
}
|
||||
|
||||
var qrCodeParts = req.body.qr_string.split("--"); // imei, serial, jwtToken
|
||||
|
||||
const device = await models.__db.get('SELECT * FROM devices WHERE imei = ? AND serial = ?', qrCodeParts[0], qrCodeParts[1]);
|
||||
if (device == null) {
|
||||
res.redirect('/useradmin/overview?linkstatus=' + encodeURIComponent('Device not registered on Server'));
|
||||
}
|
||||
var decoded = controllers.authentication.validateJWT(qrCodeParts[2], device.public_key);
|
||||
if (decoded == null || decoded.pair == undefined) {
|
||||
res.redirect('/useradmin/overview?linkstatus=' + encodeURIComponent('Device QR Token is invalid or has expired'));
|
||||
}
|
||||
if (device.account_id != 0) {
|
||||
res.redirect('/useradmin/overview?linkstatus=' + encodeURIComponent('Device is already paired, unpair in that account first'));
|
||||
}
|
||||
|
||||
const result = await models.__db.run(
|
||||
'UPDATE devices SET account_id = ? WHERE dongle_id = ?',
|
||||
account.id,
|
||||
device.dongle_id
|
||||
);
|
||||
|
||||
res.redirect('/useradmin/overview');
|
||||
}))
|
||||
|
||||
|
||||
router.get('/useradmin/device/:dongleId', runAsyncWrapper(async (req, res) => {
|
||||
const account = await controllers.authentication.getAuthenticatedAccount(req, res);
|
||||
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', config.applicationSalt).update(device.dongle_id).digest('hex');
|
||||
|
||||
const bootlogDirectoryTree = dirTree(config.storagePath + 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(config.storagePath + 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 = '<html style="font-family: monospace"><h2>Welcome To The RetroPilot Server Dashboard!</h2>' +
|
||||
|
||||
`
|
||||
<a href="/useradmin/overview">< < < Back To Overview</a>
|
||||
<br><br><h3>Device ` + device.dongle_id + `</h3>
|
||||
<b>Type:</b> ` + device.device_type + `<br>
|
||||
<b>Serial:</b> ` + device.serial + `<br>
|
||||
<b>IMEI:</b> ` + device.imei + `<br>
|
||||
<b>Registered:</b> ` + controllers.helpers.formatDate(device.created) + `<br>
|
||||
<b>Last Ping:</b> ` + controllers.helpers.formatDate(device.last_ping) + `<br>
|
||||
<b>Public Key:</b><br><span style="font-size: 0.8em">` + device.public_key.replace(/\r?\n|\r/g, "<br>") + `</span>
|
||||
<br>
|
||||
<b>Stored Drives:</b> ` + drives.length + `<br>
|
||||
<b>Quota Storage:</b> ` + device.storage_used + ` MB / ` + config.deviceStorageQuotaMb + ` MB<br>
|
||||
<br>
|
||||
`;
|
||||
|
||||
response += `<b>Boot Logs (last 5):</b><br>
|
||||
<table border=1 cellpadding=2 cellspacing=2>
|
||||
<tr><th>date</th><th>file</th><th>size</th></tr>
|
||||
`;
|
||||
for (var i = 0; i < Math.min(5, bootlogFiles.length); i++) {
|
||||
response += `<tr><td>` + controllers.helpers.formatDate(bootlogFiles[i].date) + `</td><td><a href="` + config.baseDriveDownloadUrl + device.dongle_id + "/" + dongleIdHash + "/boot/" + bootlogFiles[i].name + `" target=_blank>` + bootlogFiles[i].name + `</a></td><td>` + bootlogFiles[i].size + `</td></tr>`;
|
||||
}
|
||||
response += `</table><br><br>`;
|
||||
|
||||
response += `<b>Crash Logs (last 5):</b><br>
|
||||
<table border=1 cellpadding=2 cellspacing=2>
|
||||
<tr><th>date</th><th>file</th><th>size</th></tr>
|
||||
`;
|
||||
for (var i = 0; i < Math.min(5, crashlogFiles.length); i++) {
|
||||
response += `<tr><td>` + controllers.helpers.formatDate(crashlogFiles[i].date) + `</td><td><a href="` + config.baseDriveDownloadUrl + device.dongle_id + "/" + dongleIdHash + "/crash/" + crashlogFiles[i].name + `" target=_blank>` + crashlogFiles[i].name + `</a></td><td>` + crashlogFiles[i].size + `</td></tr>`;
|
||||
}
|
||||
response += `</table><br><br>`;
|
||||
|
||||
|
||||
response += `<b>Drives (non-preserved drives expire ` + config.deviceDriveExpirationDays + ` days after upload):</b><br>
|
||||
<table border=1 cellpadding=2 cellspacing=2>
|
||||
<tr><th>identifier</th><th>filesize</th><th>duration</th><th>distance_meters</th><th>upload_complete</th><th>is_processed</th><th>upload_date</th><th>actions</th></tr>
|
||||
`;
|
||||
|
||||
for (var i in drives) {
|
||||
response += '<tr><td><a href="/useradmin/drive/' + drives[i].dongle_id + '/' + drives[i].identifier + '">' + (drives[i].is_preserved ? '<b>' : '') + drives[i].identifier + (drives[i].is_preserved ? '</b>' : '') + '</a></td><td>' + Math.round(drives[i].filesize / 1024) + ' MiB</td><td>' + controllers.helpers.formatDuration(drives[i].duration) + '</td><td>' + Math.round(drives[i].distance_meters / 1000) + ' km</td><td>' + drives[i].upload_complete + '</td><td>' + drives[i].is_processed + '</td><td>' + controllers.helpers.formatDate(drives[i].created) + '</td><td>' + '[<a href="/useradmin/drive/' + drives[i].dongle_id + '/' + drives[i].identifier + '/delete" onclick="return confirm(\'Permanently delete this drive?\')">delete</a>]' + (drives[i].is_preserved ? '' : ' [<a href="/useradmin/drive/' + drives[i].dongle_id + '/' + drives[i].identifier + '/preserve">preserve</a>]') + '</tr>';
|
||||
}
|
||||
response += `</table>
|
||||
<br>
|
||||
<hr/>
|
||||
<a href="/useradmin/unpair_device/` + device.dongle_id + `" 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>
|
||||
<br><br>
|
||||
<hr/>
|
||||
<a href="/useradmin/signout">Sign Out</a></html>`;
|
||||
|
||||
res.status(200);
|
||||
res.send(response);
|
||||
|
||||
}))
|
||||
|
||||
|
||||
router.get('/useradmin/drive/:dongleId/:driveIdentifier/:action', runAsyncWrapper(async (req, res) => {
|
||||
const account = await controllers.authentication.getAuthenticatedAccount(req, res);
|
||||
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, res);
|
||||
|
||||
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', config.applicationSalt).update(device.dongle_id).digest('hex');
|
||||
var driveIdentifierHash = crypto.createHmac('sha256', config.applicationSalt).update(drive.identifier).digest('hex');
|
||||
|
||||
var driveUrl = config.baseDriveDownloadUrl + device.dongle_id + "/" + dongleIdHash + "/" + driveIdentifierHash + "/" + drive.identifier + "/";
|
||||
|
||||
var cabanaUrl = null;
|
||||
if (drive.is_processed) {
|
||||
cabanaUrl = config.cabanaUrl + '?retropilotIdentifier=' + device.dongle_id + '|' + dongleIdHash + '|' + drive.identifier + '|' + driveIdentifierHash + '&retropilotHost=' + encodeURIComponent(config.baseUrl) + '&demo=1"';
|
||||
}
|
||||
|
||||
const directoryTree = dirTree(config.storagePath + device.dongle_id + "/" + dongleIdHash + "/" + driveIdentifierHash + "/" + drive.identifier);
|
||||
|
||||
|
||||
var response = '<html style="font-family: monospace"><h2>Welcome To The RetroPilot Server Dashboard!</h2>' +
|
||||
`
|
||||
<a href="/useradmin/device/` + device.dongle_id + `">< < < Back To Device ` + device.dongle_id + `</a>
|
||||
<br><br><h3>Drive ` + drive.identifier + ` on ` + drive.dongle_id + `</h3>
|
||||
<b>Drive Date:</b> ` + controllers.helpers.formatDate(drive.drive_date) + `<br>
|
||||
<b>Upload Date:</b> ` + controllers.helpers.formatDate(drive.created) + `<br>
|
||||
<b>Num Segments:</b> ` + (drive.max_segment + 1) + `<br>
|
||||
<b>Storage:</b> ` + Math.round(drive.filesize / 1024) + ` MiB<br>
|
||||
<b>Duration:</b> ` + controllers.helpers.formatDuration(drive.duration) + `<br>
|
||||
<b>Distance:</b> ` + Math.round(drive.distance_meters / 1000) + ` km<br>
|
||||
<b>Is Preserved:</b> ` + drive.is_preserved + `<br>
|
||||
<b>Upload Complete:</b> ` + drive.upload_complete + `<br>
|
||||
<b>Processed:</b> ` + drive.is_processed + `<br>
|
||||
<br><br>
|
||||
` + (cabanaUrl ? '<a href="' + cabanaUrl + '" target=_blank><b>View Drive in CABANA</b></a><br><br>' : '') + `
|
||||
<b>Files:</b><br>
|
||||
<table border=1 cellpadding=2 cellspacing=2>
|
||||
<tr><th>segment</th><th>qcamera</th><th>qlog</th><th>fcamera</th><th>rlog</th><th>dcamera</th><th>processed</th><th>stalled</th></tr>
|
||||
`;
|
||||
|
||||
|
||||
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 = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
|
||||
if (directoryTree.children[i].children[c].name == 'dcamera.hevc') fcamera = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
|
||||
if (directoryTree.children[i].children[c].name == 'qcamera.ts') qcamera = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
|
||||
if (directoryTree.children[i].children[c].name == 'qlog.bz2') qlog = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
|
||||
if (directoryTree.children[i].children[c].name == 'rlog.bz2') rlog = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
|
||||
}
|
||||
|
||||
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] = '<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>';
|
||||
}
|
||||
|
||||
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 += '<tr><td>' + i + '</td><td>' + qcamera + '</td><td>' + qlog + '</td><td>' + fcamera + '</td><td>' + rlog + '</td><td>' + dcamera + '</td><td>' + isProcessed + '</td><td>' + isStalled + '</td></tr>';
|
||||
} else
|
||||
response += directorySegments["seg-" + i];
|
||||
}
|
||||
|
||||
response += `</table>
|
||||
<br><br>
|
||||
<hr/>
|
||||
<a href="/useradmin/signout">Sign Out</a></html>`;
|
||||
|
||||
res.status(200);
|
||||
res.send(response);
|
||||
|
||||
}))
|
||||
|
||||
|
||||
module.exports = (_models, _controllers, _logger) => {
|
||||
models = _models;
|
||||
controllers = _controllers;
|
||||
logger = _logger;
|
||||
|
||||
return router;
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
const jwt = require('jsonwebtoken')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const devicePrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n" +
|
||||
"MIIEowIBAAKCAQEAwhH9PqBd/R/QPvcf1Gom5Vp+zYb1+DLjiFMC7a1lNvV8MUqK\n" +
|
||||
"cKVzboq/TjkKxkPUxRRjhgt4TmxhxJ6AHAOvONMvXtS1gm8EuiJbzSUDbgr6Y3PV\n" +
|
||||
"/jHQEb8tWcmM5UZ4TV+VPBmY4w9UWJbCiJW1Udn253bqil3Mv2D4WjpxlQDNGmpc\n" +
|
||||
"Aq0b7N20WoMt/DB3Z/AnixYKLGDLmHIe8Umq9btFPv/ulVexuzeoJoYjMZLDv4Sf\n" +
|
||||
"SE4ONmDqAacjTtBPaEFedlerKVMN0PI2IzDeGvEqif98lEEVh4/3X1UP21A2Cgiy\n" +
|
||||
"nHQn92HRTR8Xkc5EYDOYEpwi97G6g+qaFtOacQIDAQABAoIBAQCFtuVZGB+KPzg5\n" +
|
||||
"mgXZUkZ4cnC55YpmN5HkJOX4oycAxgWK5MQcNzMgcAK9v7m3v5bDL3gfLJn41t5K\n" +
|
||||
"HbdBFhzNt1yFJ2Pked+06+V6pE0HrhK1IWPJH8Mv5xw1KBSnCHXtQbVOUoivsak4\n" +
|
||||
"3K8ucpAa1GY1Nw8ExPpExmh3qpsFwPFFq3ZkDdGPaxQdOGzrNwC9Z6R+XNEdh+Ub\n" +
|
||||
"N6On3McK+AmI4deW5GWdL54vHsC0MfWhdWMklPcw98o9ZVQ9V6Bzf8tRIBVB4qRB\n" +
|
||||
"pyQaRRPmkpX0s5mNCAZljLxyO1oD0yfugSZOnbbmo47BmYQiNd1WfxVXwMqR0dNE\n" +
|
||||
"js4HCW7BAoGBAPrMOFlsYpP3DLOXSHQwt+ZGcnh54NJcz97KJKQRE93H4pVUcnsn\n" +
|
||||
"VhgNTq1bYkw8CdFpPJgQgeBeX4djDyfFYEDYQnmAi0hcIFuwEV8U8LYsp7EyLWVA\n" +
|
||||
"pR+vtj5mIkdZ/j4jsYAMrQbwbweptxWiOeGqGr7vGzxOXBLDS9W4FZNZAoGBAMYY\n" +
|
||||
"iU58TTWPdUllkvxPToXv6+tjognnbatYxzrwiRRKlAuc6JPZP3qADhJ7SZNxaB88\n" +
|
||||
"aun+GZEwOITCZHkKl5oSyshb0mp9SIWlG7Nkn08/8464eAJbk7tNwtdOAHdbzKS0\n" +
|
||||
"LXpNlQ9ZGy36vE6KtGctPfGY5H4r8uIX+SlmJNTZAoGAMDDnrv8tngL9tNCgAnuO\n" +
|
||||
"CriErHO26JUe+E9dZQ1HBPmwp0MX0GRJncuIz7TcmYt703pmQ04Ats1Li+dT9S9v\n" +
|
||||
"BGbJtzElEl1pdlTJsbyDWG4SNvFOWcNnN0R7P1g+w/kd6nDPXayR3uB6ZT2OSaDn\n" +
|
||||
"gF5AT2oAkMD53j0aqFF8C9kCgYB2pTN3wpMrxSRmNWP3ojhRmAUhEqd2bxoMSjvp\n" +
|
||||
"XS98674Hxo62HqQaZqAHCbhjisTmEHWod/wwLUVsnlE2/dUW/rJdlkFMboUFJoKU\n" +
|
||||
"y2tvN8pUbL/UCa1NvaE4+wrkciL7cr7aRaVFcAULYOVv1Tt/oGU9Umln+EKcj+c3\n" +
|
||||
"mGnu4QKBgBq7yEEj99q4BoK0DhS9t/Y/akN60rPrkOetxgbpSgvLifictFg9Og0p\n" +
|
||||
"empY8kk3cQACUIKoLkbrx7mOrC/MUFWZ7H4/65QxvJWsyVvdgD3JCuX6gntgxFLR\n" +
|
||||
"gELymgXiYG6TBxfH6xcFtNrFe6DeTv8YXrKRR50Kg8kjFpvmm5s9\n" +
|
||||
"-----END RSA PRIVATE KEY-----\n"
|
||||
const devicePubKey = "-----BEGIN PUBLIC KEY-----\n" +
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwhH9PqBd/R/QPvcf1Gom\n" +
|
||||
"5Vp+zYb1+DLjiFMC7a1lNvV8MUqKcKVzboq/TjkKxkPUxRRjhgt4TmxhxJ6AHAOv\n" +
|
||||
"ONMvXtS1gm8EuiJbzSUDbgr6Y3PV/jHQEb8tWcmM5UZ4TV+VPBmY4w9UWJbCiJW1\n" +
|
||||
"Udn253bqil3Mv2D4WjpxlQDNGmpcAq0b7N20WoMt/DB3Z/AnixYKLGDLmHIe8Umq\n" +
|
||||
"9btFPv/ulVexuzeoJoYjMZLDv4SfSE4ONmDqAacjTtBPaEFedlerKVMN0PI2IzDe\n" +
|
||||
"GvEqif98lEEVh4/3X1UP21A2CgiynHQn92HRTR8Xkc5EYDOYEpwi97G6g+qaFtOa\n" +
|
||||
"cQIDAQAB\n" +
|
||||
"-----END PUBLIC KEY-----\n";
|
||||
const rougePublicKey = "-----BEGIN PUBLIC KEY-----\n" +
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsD2nKi9wqmib8kEAuyz7\n" +
|
||||
"6N2OiL5ZpCkgai02V0G/cHUdkQXGxw0gWnYaDmY4uhgQM4W1jpHkwLW3PXavyDYE\n" +
|
||||
"mCeORRS2ChYqPpJJkSQ+MO1bkR1blhixF6O39gIH5+0ZuiqnDYJIcn+DcYJrTzCz\n" +
|
||||
"HXyPvRztFuuKp1unJRi8cSL6ljq5LMjZLsuY9Eb7JmYRsXB/xHDpXysyqq1VGD5c\n" +
|
||||
"QSCJMFzykQUe4PR3AhP05SunJMA+QNhRxKUVzXyo3bpAXsRhhRr/E/jl48E22edl\n" +
|
||||
"cgXar6R9CxyHY31jdJnd9pp2KPUnNgnTBdF2w3pdN9frS9QHCDLDvLbCgd2bibSj\n" +
|
||||
"CwIDAQAB\n" +
|
||||
"-----END PUBLIC KEY-----"
|
||||
|
||||
const alreadyRegisteredEmail = "adam@adamblack.us"
|
||||
const newUserEmail = "newUser@retropilot.com"
|
||||
|
||||
function makeJWT() {
|
||||
const token = jwt.sign({ register: true }, devicePrivateKey, { algorithm: 'RS256'});
|
||||
return `JWT ${token}`
|
||||
}
|
||||
|
||||
function getImei() {
|
||||
return parseInt(Math.random().toFixed(15).replace("0.",""))
|
||||
}
|
||||
|
||||
function getSerial() {
|
||||
return crypto.randomBytes(10).toString('hex');
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
makeJWT, getImei, getSerial, rougePublicKey, devicePubKey, devicePrivateKey, alreadyRegisteredEmail, newUserEmail
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
const request = require('supertest');
|
||||
const dummyGenerator = require('./../dummyGenerator');
|
||||
let app;
|
||||
|
||||
|
||||
|
||||
module.exports = (server) => {
|
||||
app = server;
|
||||
|
||||
describe('/v2/pilotauth/ - Testing device registration', function() {
|
||||
it('Returns dongle ID on valid registration', function(done) {
|
||||
request(server)
|
||||
.post('/v2/pilotauth/')
|
||||
.query({
|
||||
imei: dummyGenerator.getImei(),
|
||||
serial: dummyGenerator.getSerial(),
|
||||
public_key: dummyGenerator.devicePubKey,
|
||||
register_token: dummyGenerator.makeJWT()
|
||||
})
|
||||
|
||||
.set('Accept', 'application/x-www-form-urlencoded')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
if (!res.body.dongle_id) throw new Error("API Failed to return dongle_id on status 200")
|
||||
})
|
||||
.end(done)
|
||||
});
|
||||
|
||||
it('Returns 400 when incorrect public key given', function(done) {
|
||||
request(server)
|
||||
.post('/v2/pilotauth/')
|
||||
.query({
|
||||
imei: dummyGenerator.getImei(),
|
||||
serial: dummyGenerator.getSerial(),
|
||||
public_key: dummyGenerator.rougePublicKey,
|
||||
register_token: dummyGenerator.makeJWT()
|
||||
})
|
||||
|
||||
.set('Accept', 'application/x-www-form-urlencoded')
|
||||
.expect('Content-Type', /text/)
|
||||
.expect(400)
|
||||
.end(done)
|
||||
});
|
||||
|
||||
it('Returns 400 when missing register_token', function(done) {
|
||||
request(server)
|
||||
.post('/v2/pilotauth/')
|
||||
.query({
|
||||
imei: dummyGenerator.getImei(),
|
||||
serial: dummyGenerator.getSerial(),
|
||||
public_key: dummyGenerator.rougePublicKey,
|
||||
register_token: ""
|
||||
})
|
||||
|
||||
.set('Accept', 'application/x-www-form-urlencoded')
|
||||
.expect('Content-Type', /text/)
|
||||
.expect(400)
|
||||
.end(done)
|
||||
});
|
||||
|
||||
it('Returns 400 when missing query', function(done) {
|
||||
request(server)
|
||||
.post('/v2/pilotauth/')
|
||||
|
||||
.set('Accept', 'application/x-www-form-urlencoded')
|
||||
.expect('Content-Type', /text/)
|
||||
.expect(400)
|
||||
.end(done)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
const request = require('supertest');
|
||||
const dummyGenerator = require('./../dummyGenerator');
|
||||
let app;
|
||||
|
||||
|
||||
|
||||
module.exports = (server) => {
|
||||
app = server;
|
||||
|
||||
describe('/useradmin', function() {
|
||||
it('Page load', function (done) {
|
||||
request(server)
|
||||
.get('/useradmin')
|
||||
.expect('Content-Type', /html/)
|
||||
.expect(200)
|
||||
.end(done)
|
||||
});
|
||||
|
||||
it('Redirect on existing session', function(done) {
|
||||
request(server)
|
||||
.get('/useradmin')
|
||||
// pull sessions from a store
|
||||
.set('Cookie', ['session=s%3Aj%3A%7B%22account%22%3A%22adam%40adamblack.us%22%2C%22expires%22%3A1653171350726%7D.cRX19pNfx6mCGZ9ZYHcUIyy5CAQVMDgKrp%2F%2Bf7NFVYA;'])
|
||||
.expect('Location', '/useradmin/overview')
|
||||
.expect(302)
|
||||
.end(done)
|
||||
});
|
||||
});
|
||||
|
||||
describe('/useradmin/register/token', function() {
|
||||
it('No duplicate emails', function (done) {
|
||||
request(server)
|
||||
.post('/useradmin/register/token')
|
||||
// TODO add dedicated DB/user account for tests to run on
|
||||
.send(`email=${dummyGenerator.alreadyRegisteredEmail}`)
|
||||
.set('Accept', 'application/x-www-form-urlencoded')
|
||||
.expect('Location', `/useradmin/register?status=${encodeURIComponent('Email is already registered')}`)
|
||||
.end(done)
|
||||
});
|
||||
|
||||
it('Accepts new accounts', function (done) {
|
||||
request(server)
|
||||
.post('/useradmin/register/token')
|
||||
// TODO add dedicated DB/user account for tests to run on
|
||||
.send(`email=${dummyGenerator.newUserEmail}`)
|
||||
.set('Accept', 'application/x-www-form-urlencoded')
|
||||
.expect(200)
|
||||
.end(done)
|
||||
});
|
||||
|
||||
});
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
var server = require('./../server')
|
||||
var request = require('supertest');
|
||||
|
||||
// TODO better way to only run tests once server is up
|
||||
describe('loading express', function () {
|
||||
it('responds to /', function testSlash(done) {
|
||||
request(server)
|
||||
.get('/')
|
||||
.expect(200, done);
|
||||
});
|
||||
it('404 everything else', function testPath(done) {
|
||||
request(server)
|
||||
.get('/foo/bar')
|
||||
.expect(404, done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
require('./routes/api.test')(server);
|
||||
require('./routes/useradmin.test')(server);
|
|
@ -4,13 +4,13 @@ const path = require('path');
|
|||
const crypto = require('crypto');
|
||||
const log4js = require('log4js');
|
||||
|
||||
import sqlite3 from 'sqlite3'
|
||||
import { open } from 'sqlite'
|
||||
const sqlite3 = require('sqlite3')
|
||||
const { open } = require('sqlite')
|
||||
|
||||
const lockfile = require('proper-lockfile');
|
||||
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
|
|
Loading…
Reference in New Issue