diff --git a/migrations/5.sql b/migrations/5.sql new file mode 100644 index 0000000..34f1001 --- /dev/null +++ b/migrations/5.sql @@ -0,0 +1,2 @@ +ALTER TABLE `devices` ADD COLUMN `ios_version` VARCHAR(64) DEFAULT NULL; +ALTER TABLE `devices` ADD COLUMN `ipa_version` VARCHAR(64) DEFAULT NULL; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 06249a4..4c422e1 100644 --- a/src/index.js +++ b/src/index.js @@ -17,9 +17,10 @@ const apiRoutes = require('./routes/api.js'); const timezones = require('../static/data/timezones.json'); // TODO: Create route classes -// TODO: iOS and IPA version // TODO: Fix devices scroll with DataTables -// TODO: Fix dropdown closes when table freshes +// TODO: Secure /api/config/:uuid endpoint with token +// TODO: Provider option to show/hide config options +// TODO: Accomodate for # in uuid name const defaultData = { title: config.title, @@ -36,6 +37,7 @@ dbMigrator.load(); app.set('view engine', 'mustache'); app.set('views', path.resolve(__dirname, 'views')); app.engine('mustache', mustacheExpress()); +app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' })); // for parsing application/x-www-form-urlencoded //app.use(bodyParser.raw({ type: 'application/x-www-form-urlencoded' })); app.use(express.static(path.resolve(__dirname, '../static'))); @@ -53,7 +55,7 @@ app.use('/api', apiRoutes); // Login middleware app.use(function(req, res, next) { - if (req.path === '/api/login' || req.path === '/login' || req.path.includes('/api/config/')) { + if (req.path === '/api/login' || req.path === '/login' || req.path.includes('/api/config')) { return next(); } if (req.session.loggedin) { @@ -256,7 +258,31 @@ app.get('/schedule/delete/:name', function(req, res) { // Settings UI Routes app.get('/settings', function(req, res) { - res.render('settings', defaultData); + var data = defaultData; + data.title = config.title; + data.host = config.db.host; + data.port = config.db.port; + data.username = config.db.username; + data.password = config.db.password; + data.database = config.db.database; + data.charset = config.db.charset; + data.styles = [ + { 'name': 'dark' }, + { 'name': 'light' } + ]; + data.styles.forEach(function(style) { + style.selected = style.name === config.style; + }); + data.languages = [ + { 'name': 'en' }, + { 'name': 'es' } + ]; + data.languages.forEach(function(locale) { + locale.selected = locale.name === config.locale; + }); + data.logging = config.logging ? 'checked' : ''; + console.log('Settings:', data); + res.render('settings', data); }); app.listen(config.port, config.interface, () => console.log(`Listening on port ${config.port}...`)); \ No newline at end of file diff --git a/src/models/config.js b/src/models/config.js index 6359f1f..11e73ed 100644 --- a/src/models/config.js +++ b/src/models/config.js @@ -4,7 +4,7 @@ const query = require('../db.js'); class Config { constructor(name, backendUrl, dataEndpoints, token, heartbeatMaxTime, minDelayLogout, - accountManager, deployEggs, nearbyTracker, autoLogin, isDefault) { + accountManager, deployEggs, nearbyTracker, autoLogin, isDefault) { this.name = name; this.backendUrl = backendUrl; this.dataEndpoints = dataEndpoints; @@ -50,13 +50,13 @@ class Config { return data; } static async create(name, backendUrl, dataEndpoints, token, heartbeatMaxTime, minDelayLogout, - accountManager, deployEggs, nearbyTracker, autoLogin, isDefault) { + accountManager, deployEggs, nearbyTracker, autoLogin, isDefault) { var sql = ` INSERT INTO configs (name, backend_url, data_endpoints, token, heartbeat_max_time, min_delay_logout, account_manager, deploy_eggs, nearby_tracker, auto_login, is_default) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; var args = [name, backendUrl, dataEndpoints, token, heartbeatMaxTime, minDelayLogout, - accountManager, deployEggs, nearbyTracker, autoLogin, isDefault]; + accountManager, deployEggs, nearbyTracker, autoLogin, isDefault]; var result = await query(sql, args); return result.affectedRows === 1; } diff --git a/src/models/device.js b/src/models/device.js index fb340f2..5417f64 100644 --- a/src/models/device.js +++ b/src/models/device.js @@ -3,19 +3,21 @@ const query = require('../db.js'); class Device { - constructor(uuid, config, lastSeen, clientip) { + constructor(uuid, config, lastSeen, clientip, iosVersion, ipaVersion) { this.uuid = uuid; this.config = config; this.lastSeen = lastSeen; this.clientip = clientip; + this.iosVersion = iosVersion; + this.ipaVersion = ipaVersion; } static async getAll() { - var devices = await query('SELECT uuid, config, last_seen, clientip FROM devices'); + var devices = await query('SELECT uuid, config, last_seen, clientip, ios_version, ipa_version FROM devices'); return devices; } static async getByName(uuid) { var sql = ` - SELECT uuid, config, last_seen, clientip + SELECT uuid, config, last_seen, clientip, ios_version, ipa_version FROM devices WHERE uuid = ?`; var args = [uuid]; @@ -27,21 +29,25 @@ class Device { result[0].uuid, result[0].config, result[0].last_seen, - result[0].clientip + result[0].clientip, + result[0].ios_version, + result[0].ipa_version ); } - static async create(uuid, config = null, lastSeen = null, clientip = null) { + static async create(uuid, config = null, lastSeen = null, clientip = null, iosVersion = null, ipaVersion = null) { var sql = ` - INSERT INTO devices (uuid, config, last_seen, clientip) - VALUES (?, ?, ?, ?)`; - var args = [uuid, config, lastSeen, clientip]; + INSERT INTO devices (uuid, config, last_seen, clientip, ios_version, ipa_version) + VALUES (?, ?, ?, ?, ?, ?)`; + var args = [uuid, config, lastSeen, clientip, iosVersion, ipaVersion]; var result = await query(sql, args); if (result.affectedRows === 1) { return new Device( uuid, config, lastSeen, - clientip + clientip, + iosVersion, + ipaVersion ); } return null; @@ -55,9 +61,9 @@ class Device { async save() { var sql = ` UPDATE devices - SET config = ?, last_seen = ?, clientip = ? + SET config = ?, last_seen = ?, clientip = ?, ios_version = ?, ipa_version = ? WHERE uuid = ?`; - var args = [this.config, this.lastSeen, this.clientip, this.uuid]; + var args = [this.config, this.lastSeen, this.clientip, this.iosVersion, this.ipaVersion, this.uuid]; var result = await query(sql, args); return result.affectedRows === 1; } diff --git a/src/models/log.js b/src/models/log.js index 74e205c..21f755d 100644 --- a/src/models/log.js +++ b/src/models/log.js @@ -15,7 +15,7 @@ class Log { this.timestamp = timestamp; this.message = message; } - static async getByDevice(uuid) { + static getByDevice(uuid) { var name = uuid + '.log'; var logFile = path.resolve(logsDir, name); if (!fs.existsSync(logFile)) { @@ -35,7 +35,7 @@ class Log { }); return logs; } - static async create(uuid, message) { + static create(uuid, message) { var name = uuid + '.log'; var logFile = path.resolve(logsDir, name); var msg = { @@ -47,7 +47,7 @@ class Log { if (err) throw err; }); } - static async delete(uuid) { + static delete(uuid) { var name = uuid + '.log'; var logFile = path.resolve(logsDir, name); if (fs.existsSync(logFile)) { @@ -56,7 +56,7 @@ class Log { } return false; } - static async deleteAll() { + static deleteAll() { fs.readdir(logsDir, function(err, files) { if (err) throw err; files.forEach(function(file) { diff --git a/src/routes/api.js b/src/routes/api.js index cd7b914..5074523 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -69,14 +69,43 @@ router.post('/account/change_password/:username', async function(req, res) { }); +// Settings API Routes +router.post('/settings/change_ui', function(req, res) { + var data = req.body; + var newConfig = config; + newConfig.title = data.title; + newConfig.locale = data.locale; + newConfig.style = data.style; + newConfig.logging = data.logging === 'on' ? 1 : 0; + fs.writeFileSync(path.resolve(__dirname, '../config.json'), JSON.stringify(newConfig, null, 2)); + res.redirect('/settings'); +}); + +router.post('/settings/change_db', function(req, res) { + var data = req.body; + var newConfig = config; + newConfig.db.host = data.host; + newConfig.db.port = data.port; + newConfig.db.username = data.username; + newConfig.db.password = data.password; + newConfig.db.database = data.database; + newConfig.db.charset = data.charset; + fs.writeFileSync(path.resolve(__dirname, '../config.json'), JSON.stringify(newConfig, null, 2)); + res.redirect('/settings'); +}); + + // Device API Routes router.get('/devices', async function(req, res) { try { var devices = await Device.getAll(); devices.forEach(function(device) { var exists = fs.existsSync(path.join(screenshotsDir, device.uuid + '.png')); - var image = exists ? `/screenshots/${device.uuid}.png` : '/img/offline.png'; - device.image = ``; + // Device received a config last 15 minutes + var delta = 15 * 60; + var isOffline = device.last_seen > (Math.round((new Date()).getTime() / 1000) - delta) ? 0 : 1; + var image = exists ? `/screenshots/${device.uuid}.png` : (isOffline ? '/img/offline.png' : '/img/online.png'); + device.image = ``; device.last_seen = utils.getDateTime(device.last_seen); device.buttons = `
@@ -167,12 +196,15 @@ router.get('/configs', async function(req, res) { } }); -router.get('/config/:uuid', async function(req, res) { - var uuid = req.params.uuid; +router.post('/config', async function(req, res) { + var data = req.body; + var uuid = data.uuid; + var iosVersion = data.ios_version; + var ipaVersion = data.ipa_version; var device = await Device.getByName(uuid); var noConfig = false; var assignDefault = false; - // Check for a proxied IP before the normal IP and set the first one at exists + // Check for a proxied IP before the normal IP and set the first one that exists var clientip = ((req.headers['x-forwarded-for'] || '').split(', ')[0]) || (req.connection.remoteAddress).match('[0-9]+.[0-9].+[0-9]+.[0-9]+$')[0]; console.log('[' + new Date().toLocaleString() + ']', 'Client', uuid, 'at', clientip, 'is requesting a config.'); @@ -181,6 +213,8 @@ router.get('/config/:uuid', async function(req, res) { // Device exists device.lastSeen = new Date() / 1000; device.clientip = clientip; + device.iosVersion = iosVersion; + device.ipaVersion = ipaVersion; device.save(); if (device.config) { // Nothing to do besides respond with config @@ -192,7 +226,8 @@ router.get('/config/:uuid', async function(req, res) { } else { console.log('Device does not exist, creating...'); // Device doesn't exist, create db entry - device = await Device.create(uuid, null, new Date() / 1000, clientip); // REVIEW: Maybe return Device object upon creation to prevent another sql call to get Device object? + var ts = new Date() / 1000; + device = await Device.create(uuid, null, ts, clientip, iosVersion, ipaVersion); if (device) { // Success, assign default config if there is one. assignDefault = true; @@ -227,11 +262,11 @@ router.get('/config/:uuid', async function(req, res) { var c = await Config.getByName(device.config); if (c === null) { console.error('Failed to grab config', device.config); - var data = { + var noConfigData2 = { status: 'error', error: 'Device not assigned to config!' }; - res.send(JSON.stringify(data)); + res.send(JSON.stringify(noConfigData2)); return; } // Build json config @@ -397,9 +432,9 @@ router.get('/schedule/delete_all', function(req, res) { // Logging API requests -router.get('/logs/:uuid', async function(req, res) { +router.get('/logs/:uuid', function(req, res) { var uuid = req.params.uuid; - var logs = await Log.getByDevice(uuid); + var logs = Log.getByDevice(uuid); res.send({ uuid: uuid, data: { @@ -408,33 +443,37 @@ router.get('/logs/:uuid', async function(req, res) { }); }); -router.post('/log/new/:uuid', async function(req, res) { +router.post('/log/new', function(req, res) { if (config.logging === false) { // Logs are disabled res.send('OK'); return; } - var uuid = req.params.uuid; - var msg = Object.keys(req.body)[0]; // Dumb hack - var result = await Log.create(uuid, msg); - if (result) { - // Success + var uuid = req.body.uuid; + var messages = req.body.messages; + if (messages) { + messages.forEach(function(message) { + var result = Log.create(uuid, message); + if (result) { + // Success + } + console.log('[SYSLOG]', uuid, ':', message); + }); } - console.log('[SYSLOG]', uuid, ':', msg); res.send('OK'); }); -router.get('/log/delete/:uuid', async function(req, res) { +router.get('/log/delete/:uuid', function(req, res) { var uuid = req.params.uuid; - var result = await Log.delete(uuid); + var result = Log.delete(uuid); if (result) { // Success } res.redirect('/device/logs/' + uuid); }); -router.get('/logs/delete_all', async function(req, res) { - var result = await Log.deleteAll(); +router.get('/logs/delete_all', function(req, res) { + var result = Log.deleteAll(); if (result) { // Success } diff --git a/src/utils.js b/src/utils.js index 03bf631..1be3e7c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -18,7 +18,7 @@ function getDateTime(timestamp) { } function buildConfig(backendUrl, dataEndpoints, token, heartbeatMaxTime, minDelayLogout, - accountManager, deployEggs, nearbyTracker, autoLogin) { + accountManager, deployEggs, nearbyTracker, autoLogin) { var obj = { 'backend_url': backendUrl, 'data_endpoints': (dataEndpoints || '').split(',') || [], diff --git a/src/views/device-manage.mustache b/src/views/device-manage.mustache index e76442c..59c511c 100644 --- a/src/views/device-manage.mustache +++ b/src/views/device-manage.mustache @@ -116,13 +116,23 @@ } function get(url, id) { + if (url.includes('/screen')) { + + } + var isScreen = url.includes('/screen'); $.ajax({ url: url, type: 'GET', - dataType: 'json', + dataType: isScreen ? 'image/jpeg' : 'json', success: function (data) { console.log("Ajax response:", data); - $('#' + id).text(JSON.stringify(data, null, 2)); + if (isScreen) { + img = new Image(); + img.src = data; + $('#' + id).append(img); + } else { + $('#' + id).text(JSON.stringify(data, null, 2)); + } }, error: function (error) { console.log("Error:", error); diff --git a/src/views/devices.mustache b/src/views/devices.mustache index 601a281..76944d1 100644 --- a/src/views/devices.mustache +++ b/src/views/devices.mustache @@ -14,6 +14,8 @@ Preview UUID Config + iOS Version + IPA Version Last Seen @@ -44,6 +46,8 @@ { "data": "image" }, { "data": "uuid" }, { "data": "config" }, + { "data": "ios_version" }, + { "data": "ipa_version" }, { "data": "last_seen" }, { "data": "buttons" }, ], @@ -51,7 +55,7 @@ "order": [[ 1, "asc" ]], "search.caseInsensitive": true, "columnDefs": [ { - "targets": [4], + "targets": [6], "orderable": false }], "responsive": true diff --git a/src/views/index.mustache b/src/views/index.mustache index d00b5d0..3540f52 100644 --- a/src/views/index.mustache +++ b/src/views/index.mustache @@ -7,7 +7,7 @@
-
Overivew
+
Overview
diff --git a/src/views/settings.mustache b/src/views/settings.mustache index 4154f82..e5213a6 100644 --- a/src/views/settings.mustache +++ b/src/views/settings.mustache @@ -3,7 +3,7 @@ {{> navbar}}
-

Settings (NOT WORKING YET)

+

Settings


@@ -13,22 +13,24 @@
Title - +
Locale
Theme - + + {{#styles}} + + {{/styles}}
@@ -49,27 +51,27 @@
Host - +
Port - +
Username - +
Password - +
Database - +
Character Set - +