Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,7 @@ config.json
logs

# No screenshots
screenshots
screenshots

# No schedules
schedules.json
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ Central repository for macless client configurations without having to keep trac
You can also pre-create devices and assign configs yourself if needed.

## Features
- Custom config assignments
- Screenshot preview
- Device logging
- Device endpoint tooling
- and more...
- Custom config assignments
- Screenshot preview
- Device logging
- Device endpoint tooling
- and more...

## Installation
1.) Clone repository `git clone https://github.com/versx/DeviceConfigManager`
2.) Install dependencies `npm install`
3.) Copy config `cp src/config.example.json src/config.json`
4.) Fill out config `vi src/config.json`
5.) Run `npm run start`
6.) Access via http://machineip:port/ using username: `root` and password `pass123!`

## Notes
If you use HAProxy, make sure to set `option forwardfor` in your haproxy.cfg if you are not passing the x-forward-for header so the correct IP addresses are saved.

## PM2 (recommended)
Once everything is setup and running appropriately, you can add this to PM2 ecosystem.config.js file so it is automatically started:
Expand Down
6 changes: 6 additions & 0 deletions migrations/3.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(255) NOT NULL
);

INSERT INTO `users` (`username`, `password`) VALUES ('root', SHA1('pass123!'));
58 changes: 58 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"moment-timezone": "^0.5.28",
"express-session": "^1.17.1",
"multer": "^1.4.2",
"mustache": "^4.0.1",
"mustache-express": "^1.3.0",
Expand Down
1 change: 1 addition & 0 deletions src/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"locale": "en",
"style": "dark",
"logging": true,
"secret": "-/!sup3rr4nd0m70p53cr3t70k3ny0u5h0u1dch4ng3!/-",
"db": {
"host": "127.0.0.1",
"port": 3306,
Expand Down
4 changes: 3 additions & 1 deletion src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ function query(sql, args) {
// The Promise constructor should catch any errors thrown on
// this tick. Alternately, try/catch and reject(err) on catch.
var conn = getConnection();
/* eslint-disable no-unused-vars */
conn.query(sql, args, function(err, rows, fields) {
/* eslint-enable no-unused-vars */
// Call reject on error states,
// call resolve with results
if (err) {
Expand All @@ -40,7 +42,7 @@ function query(sql, args) {
resolve(rows);
conn.end(function(err, args) {
if (err) {
console.error('Failed to close mysql connection.');
console.error('Failed to close mysql connection:', args);
return;
}
});
Expand Down
133 changes: 119 additions & 14 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@

const path = require('path');
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();
const mustacheExpress = require('mustache-express');

const config = require('./config.json');
const Device = require('./models/device.js');
const Config = require('./models/config.js');
const Migrator = require('./migrator.js');
const ScheduleManager = require('./models/schedule-manager.js');
const apiRoutes = require('./routes/api.js');

const timezones = require('../static/data/timezones.json');

// TODO: Create route classes
// TODO: Error checking/handling
// TODO: Security / token auth / users (maybe?) or basic authentication

const defaultData = {
title: config.title,
locale: config.locale,
style: config.style == 'dark' ? 'dark' : '',
logging: config.logging
};

// Start database migrator
var dbMigrator = new Migrator();
Expand All @@ -24,29 +35,69 @@ app.set('view engine', 'mustache');
app.set('views', path.resolve(__dirname, 'views'));
app.engine('mustache', mustacheExpress());
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')));
app.use('/screenshots', express.static(path.resolve(__dirname, '../screenshots')));

const defaultData = {
title: config.title,
locale: config.locale,
style: config.style == 'dark' ? 'dark' : '',
logging: config.logging
};
// Sessions middleware
app.use(session({
secret: config.secret, // REVIEW: Randomize?
resave: true,
saveUninitialized: true
}));

// Login middleware
app.use(function(req, res, next) {
if (req.path === '/api/login' || req.path === '/login') {
return next();
}
if (req.session.loggedin) {
defaultData.logged_in = true;
next();
return;
}
res.redirect('/login');
});

// API Route
app.use('/api', apiRoutes);

// UI Routes
app.get(['/', '/index'], async function(req, res) {
var devices = await Device.getAll();
var configs = await Config.getAll();
var metadata = await Migrator.getEntries();
if (req.session.loggedin) {
var username = req.session.username;
var devices = await Device.getAll();
var configs = await Config.getAll();
var schedules = ScheduleManager.getAll();
var metadata = await Migrator.getEntries();
var data = defaultData;
data.metadata = metadata;
data.devices = devices.length;
data.configs = configs.length;
data.schedules = Object.keys(schedules).length;
data.username = username;
res.render('index', data);
}
});

app.get('/login', function(req, res) {
var data = defaultData;
data.logged_in = false;
data.username = null;
res.render('login', data);
});

app.get('/logout', function(req, res) {
req.session.destroy(function(err) {
if (err) throw err;
res.redirect('/login');
});
});

app.get('/account', function(req, res) {
var data = defaultData;
data.metadata = metadata;
data.devices = devices.length;
data.configs = configs.length;
res.render('index', data);
data.username = req.session.username;
res.render('account', data);
});

// Device UI Routes
Expand Down Expand Up @@ -163,6 +214,60 @@ app.get('/config/delete/:name', function(req, res) {
res.render('config-delete', data);
});

// Schedule UI Routes
app.get('/schedules', function(req, res) {
res.render('schedules', defaultData);
});

app.get('/schedule/new', async function(req, res) {
var data = defaultData;
var configs = await Config.getAll();
var devices = await Device.getAll();
data.configs = configs;
data.devices = devices;
data.timezones = timezones;
res.render('schedule-new', data);
});

app.get('/schedule/edit/:name', async function(req, res) {
var name = req.params.name;
var data = defaultData;
var configs = await Config.getAll();
var devices = await Device.getAll();
var schedule = ScheduleManager.getByName(name);
if (configs) {
configs.forEach(function(cfg) {
cfg.selected = cfg.name === schedule.config;
cfg.next_config_selected = cfg.name === schedule.next_config;
});
}
if (devices) {
devices.forEach(function(device) {
device.selected = schedule.uuids.includes(device.uuid);
});
}
data.old_name = name;
data.name = schedule.name;
data.configs = configs;
data.devices = devices;
data.start_time = schedule.start_time;
data.end_time = schedule.end_time;
data.timezone = schedule.timezone;
data.next_config = schedule.next_config;
data.enabled = schedule.enabled === 1 ? 'checked' : '';
timezones.forEach(function(timezone) {
timezone.selected = timezone.value === schedule.timezone;
});
data.timezones = timezones;
res.render('schedule-edit', data);
});

app.get('/schedule/delete/:name', function(req, res) {
var data = defaultData;
data.name = req.params.name;
res.render('schedule-delete', data);
});

// Settings UI Routes
app.get('/settings', function(req, res) {
res.render('settings', defaultData);
Expand Down
12 changes: 7 additions & 5 deletions src/migrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ class Migrator {
var done = false;
while (!done) {
if (query === undefined || query === null) {
var message = `Failed to connect to database (as root) while initializing. Try: ${count}/10`;
console.error(`[DBController] Failed to connect to database (as root) while initializing. Try: ${count}/10`);
if (count === 10) {
console.error('[DBController]', message);
process.exit(-1);
} else {
console.error('[DBController]', message);
}
count++;
await utils.snooze(2500);
Expand Down Expand Up @@ -87,7 +84,8 @@ class Migrator {
sqlSplit.forEach(async sql => {
let msql = sql.replace('&semi', ';').trim();
if (msql !== '') {
let results = await query(msql)
console.log('[DBController] Executing:', msql);
var result = await query(msql)
.then(x => x)
.catch(async err => {
console.error('[DBController] Migration failed:', err, '\r\nExecuting SQL statement:', msql);
Expand All @@ -108,6 +106,7 @@ class Migrator {
return null;
*/
});
console.log('[DBController] Migration execution result:', result);
}
});

Expand All @@ -123,6 +122,9 @@ class Migrator {
process.exit(-1);
});
console.log('[DBController] Migration successful');
if (newVersion === toVersion) {
console.log('[DBController] Migration done');
}
this.migrate(newVersion, toVersion);
}
}
Expand Down
Loading