From 7b742d475098980298dbb461341e4304553f4694 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Tue, 1 Sep 2020 16:13:40 +0000 Subject: [PATCH] Declutter index.js and add basic auth to protect some routes --- app.js | 145 +++++++++++++++++++++++++++++++++++++++++++++- config.js | 16 +++++ db.js | 145 +--------------------------------------------- event.js | 127 +++++++++++++++++++++++++++++++++++++++- index.js | 135 +++--------------------------------------- package-lock.json | 16 +++++ package.json | 1 + 7 files changed, 313 insertions(+), 272 deletions(-) create mode 100644 config.js diff --git a/app.js b/app.js index 95edfd1..3ec1e3f 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,15 @@ +import express from 'express'; +import basicAuth from 'express-basic-auth'; import { randomId } from './util.js'; +import { basicAuthConfig } from './config.js' +import pool from './db.js'; + +pool.query(`CREATE TABLE IF NOT EXISTS apps ( + id VARCHAR(32) PRIMARY KEY, + name VARCHAR(256) UNIQUE NOT NULL +)`, (err, res) => { + if (err) console.error(err); +}); export default class App { id = randomId(32); @@ -7,4 +18,136 @@ export default class App { constructor(name) { this.name = name; } -} \ No newline at end of file +} + +export class AppRepository { + static getApps() { + return new Promise((resolve, reject) => { + pool.query('SELECT * FROM apps', (err, res) => { + if (err) { + reject(err); + return; + } + + resolve(res); + }); + }) + } + + static getApp(appId) { + return new Promise((resolve, reject) => { + pool.query('SELECT * FROM apps WHERE id = ? LIMIT 1', appId, (err, res) => { + if (err) { + reject(err); + return; + } + + resolve(res[0]); + }); + }) + } + + static createApp(app) { + return new Promise((resolve, reject) => { + pool.query('INSERT INTO apps SET ?', app, (err, res, fields) => { + if (err) { + reject(err); + return; + } + + resolve(app); + }); + }) + } + + static updateApp(appId, name) { + return new Promise((resolve, reject) => { + pool.query('UPDATE apps SET name = ? WHERE id = ?', [name, appId], (err, res, fields) => { + if (err) { + reject(err); + return; + } + + resolve(res.affectedRows === 1); + }); + }) + } + + static deleteApp(appId) { + return new Promise((resolve, reject) => { + pool.query('DELETE FROM apps WHERE id = ?', appId, (err, res) => { + if (err) { + reject(err); + return; + } + + resolve(res); + }); + }) + } +} + +export const appRouter = express.Router(); +appRouter.use(basicAuth(basicAuthConfig)); + +appRouter.get('/', (req, res) => { + AppRepository.getApps() + .then((apps) => { + res.json(apps); + }).catch((err) => { + console.error(err); + res.status(500).send(); + }) +}); + +appRouter.post('/', basicAuth(basicAuthConfig), (req, res) => { + const name = req.body.name; + if (!name) { + res.status(400).send({ message: 'Invalid app name' }); + return; + } + + AppRepository.createApp(new App(name)) + .then((app) => { + res.json(app); + }).catch((err) => { + res.status(500).send(); + }) +}); + +appRouter.get('/:appId', basicAuth(basicAuthConfig), (req, res) => { + AppRepository.getApp(req.params.appId) + .then((app) => { + if (!app) { + res.sendStatus(404); + } else { + res.json(app); + } + }).catch((err) => { + console.error(err); + res.status(500).send(); + }) +}) + +appRouter.patch('/:appId', (req, res) => { + AppRepository.updateApp(req.params.appId, req.body.name) + .then((app) => { + if (!app) { + res.sendStatus(404); + } else { + res.sendStatus(204); + } + }).catch((err) => { + console.error(err); + res.status(500).send(); + }) +}) + +appRouter.delete('/:appId', (req, res) => { + AppRepository.deleteApp(req.params.appId) + .then((app) => { + res.send(204); + }).catch((err) => { + res.status(500).send(); + }) +}) diff --git a/config.js b/config.js new file mode 100644 index 0000000..d77e10c --- /dev/null +++ b/config.js @@ -0,0 +1,16 @@ +const users = {}; +users[process.env.ADMIN_USER || 'admin'] = process.env.ADMIN_USER || 'flayre' +export const basicAuthConfig = { + users: users +}; + +export const dbConfig = { + connectionLimit: process.env.DB_POOL_SIZE || 10, + host: process.env.DB_HOST || 'localhost', + host: process.env.DB_PORT || 3306, + user: process.env.DB_USER || 'flayre', + password: process.env.DB_PASSWORD || 'flayre', + database: process.env.DB_NAME || 'flayre' +} + +export const port = process.env.PORT || 3000; diff --git a/db.js b/db.js index 2051318..ca3d68f 100644 --- a/db.js +++ b/db.js @@ -1,145 +1,6 @@ -import App from './app.js'; -import Event from './event.js'; import mysql from 'mysql'; +import { dbConfig } from './config.js'; -const pool = mysql.createPool({ - connectionLimit: process.env.DB_POOL_SIZE || 10, - host: process.env.DB_HOST || 'localhost', - host: process.env.DB_PORT || 3306, - user: process.env.DB_USER || 'flayre', - password: process.env.DB_PASSWORD || 'flayre', - database: process.env.DB_NAME || 'flayre' -}); +const pool = mysql.createPool(dbConfig); -pool.query(`CREATE TABLE IF NOT EXISTS apps ( - id VARCHAR(32) PRIMARY KEY, - name VARCHAR(256) UNIQUE NOT NULL -)`, (err, res) => { - if (err) console.error(err); -}); - -pool.query(`CREATE TABLE IF NOT EXISTS events ( - id VARCHAR(32) PRIMARY KEY, - appId VARCHAR(32) NOT NULL, - date DATETIME NOT NULL, - userAgent VARCHAR(256), - platform VARCHAR(32), - manufacturer VARCHAR(256), - model VARCHAR(256), - version VARCHAR(32), - locale VARCHAR(8), - sessionId VARCHAR(32), - data TEXT DEFAULT NULL, - type VARCHAR(256) DEFAULT NULL, - FOREIGN KEY (appId) - REFERENCES apps(id) - ON DELETE CASCADE -)`); - -export class AppRepository { - static getApps() { - return new Promise((resolve, reject) => { - pool.query('SELECT * FROM apps', (err, res) => { - if (err) { - reject(err); - return; - } - - resolve(res); - }); - }) - } - - static getApp(appId) { - return new Promise((resolve, reject) => { - pool.query('SELECT * FROM apps WHERE id = ? LIMIT 1', appId, (err, res) => { - if (err) { - reject(err); - return; - } - - resolve(res[0]); - }); - }) - } - - static createApp(app) { - return new Promise((resolve, reject) => { - pool.query('INSERT INTO apps SET ?', app, (err, res, fields) => { - if (err) { - reject(err); - return; - } - - resolve(app); - }); - }) - } - - static updateApp(appId, name) { - return new Promise((resolve, reject) => { - pool.query('UPDATE apps SET name = ? WHERE id = ?', [name, appId], (err, res, fields) => { - if (err) { - reject(err); - return; - } - - resolve(res.affectedRows === 1); - }); - }) - } - - static deleteApp(appId) { - return new Promise((resolve, reject) => { - pool.query('DELETE FROM apps WHERE id = ?', appId, (err, res) => { - if (err) { - reject(err); - return; - } - - resolve(res); - }); - }) - } -} - -export class EventRepository { - static getEvents( - appId, - from, - to, - count, - page, - ) { - return new Promise((resolve, reject) => { - let query = 'SELECT * FROM events WHERE appId = ?'; - let queryParams = [appId] - if (from) { - query += ' AND date >= ?' - queryParams.push(from) - } - if (to) { - query += ' AND date <= ?' - queryParams.push(to) - } - if (count) { - let limit = count; - let offset = 0; - if (page) { - offset = count * (page - 1); - limit = count * page; - } - query += ' LIMIT ?,?'; - queryParams.push(offset, limit); - } - pool.query(query, queryParams, (err, res) => { - if (err) { - reject(err); - return; - } - - resolve(res); - }); - }); - } -} \ No newline at end of file +export default pool diff --git a/event.js b/event.js index 33d6a9e..ca37c23 100644 --- a/event.js +++ b/event.js @@ -1,4 +1,8 @@ -import { randomId } from './util.js'; +import { randomId, firstOfMonth, lastOfMonth } from './util.js'; +import pool from './db.js'; +import express from 'express'; +import basicAuth from 'express-basic-auth'; +import { basicAuthConfig } from './config.js' export default class Event { static types = [ @@ -65,3 +69,124 @@ export default class Event { this.type = type; } } + +pool.query(`CREATE TABLE IF NOT EXISTS events ( + id VARCHAR(32) PRIMARY KEY, + appId VARCHAR(32) NOT NULL, + date DATETIME NOT NULL, + userAgent VARCHAR(256), + platform VARCHAR(32), + manufacturer VARCHAR(256), + model VARCHAR(256), + version VARCHAR(32), + locale VARCHAR(8), + sessionId VARCHAR(32), + data TEXT DEFAULT NULL, + type VARCHAR(256) DEFAULT NULL, + FOREIGN KEY (appId) + REFERENCES apps(id) + ON DELETE CASCADE +)`); + +export class EventRepository { + static getEvents( + appId, + from, + to, + count, + page, + ) { + return new Promise((resolve, reject) => { + let query = 'SELECT * FROM events WHERE appId = ?'; + let queryParams = [appId] + if (from) { + query += ' AND date >= ?' + queryParams.push(from) + } + if (to) { + query += ' AND date <= ?' + queryParams.push(to) + } + if (count) { + let limit = count; + let offset = 0; + if (page) { + offset = count * (page - 1); + limit = count * page; + } + query += ' LIMIT ?,?'; + queryParams.push(offset, limit); + } + pool.query(query, queryParams, (err, res) => { + if (err) { + reject(err); + return; + } + + resolve(res); + }); + }); + } +} + +export const eventRouter = express.Router() +eventRouter.get('/', basicAuth(basicAuthConfig), (req, res) => { + const appId = req.query.appId; + if (!appId) { + res.status(400).send({ message: 'Invalid appId' }); + } + const from = req.query.from || firstOfMonth() + const to = req.query.to || lastOfMonth() + const count = req.query.count || 1000; + const page = req.query.count || 1; + EventRepository.getEvents(appId, from, to, count, page) + .then((events) => { + res.json(events); + }).catch((err) => { + res.status(500).send(); + }) +}); + +// This is one of the few routes that don't require authentication. Since +// events will be coming from all over the place, I don't think it makes +// sense to try to put auth in front of this. Even some kind of client +// "secret" would be trivial to deduce by examining the requests. +eventRouter.post('/', (req, res) => { + if (typeof req.body.appId === "undefined") { + res.status(400).json({ message: 'Invalid appId' }); + return; + } + + if (typeof req.body.sessionId === "undefined") { + res.status(400).json({ message: 'Invalid sessionId' }); + return; + } + + if (Event.types.indexOf(req.body.type) === -1) { + res.status(400).json({ message: 'Invalid event type' }); + return; + } + + if (typeof req.body.data === "undefined") { + // TODO: Handle data validation better than this + res.status(400).json({ message: 'Invalid data' }); + return; + } + + const event = new Event( + req.body.appId, + req.body.date, + req.body.userAgent, + req.body.platform, + req.body.manufacturer, + req.body.model, + req.body.version, + req.body.locale, + req.body.sessionId, + req.body.data, + req.body.type, + ); + + events.push(event); + res.json(event); +}); diff --git a/index.js b/index.js index 045dfb5..de06c5c 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,12 @@ import express from 'express'; -import Event from './event.js'; -import { randomId, firstOfMonth, lastOfMonth } from './util.js'; -import { AppRepository, EventRepository } from './db.js'; -import App from './app.js'; +import { eventRouter } from './event.js'; +import { port } from './config.js'; +import { randomId } from './util.js'; +import { appRouter } from './app.js'; const app = express(); app.use(express.json()); -let events = []; - app.get('/', (req, res) => { res.send('Hello, world!'); }); @@ -18,128 +16,9 @@ app.get('/id', (req, res) => { res.send(randomId(length)); }); -app.get('/apps', (req, res) => { - AppRepository.getApps() - .then((apps) => { - res.json(apps); - }).catch((err) => { - console.error(err); - res.status(500).send(); - }) -}); - -app.post('/apps', (req, res) => { - const name = req.body.name; - if (!name) { - res.status(400).send({ message: 'Invalid app name' }); - return; - } - - AppRepository.createApp(new App(name)) - .then((app) => { - res.json(app); - }).catch((err) => { - res.status(500).send(); - }) -}); - -app.get('/apps/:appId', (req, res) => { - AppRepository.getApp(req.params.appId) - .then((app) => { - if (!app) { - res.sendStatus(404); - } else { - res.json(app); - } - }).catch((err) => { - console.error(err); - res.status(500).send(); - }) -}) - -app.patch('/apps/:appId', (req, res) => { - AppRepository.updateApp(req.params.appId, req.body.name) - .then((app) => { - if (!app) { - res.sendStatus(404); - } else { - res.sendStatus(204); - } - }).catch((err) => { - console.error(err); - res.status(500).send(); - }) -}) - -app.delete('/apps/:appId', (req, res) => { - AppRepository.deleteApp(req.params.appId) - .then((app) => { - res.send(204); - }).catch((err) => { - res.status(500).send(); - }) -}) - -app.get('/events', (req, res) => { - const appId = req.query.appId; - if (!appId) { - res.status(400).send({ message: 'Invalid appId' }); - } - const from = req.query.from || firstOfMonth() - const to = req.query.to || lastOfMonth() - const count = req.query.count || 1000; - const page = req.query.count || 1; - EventRepository.getEvents(appId, from, to, count, page) - .then((events) => { - res.json(events); - }).catch((err) => { - res.status(500).send(); - }) -}); - -app.post('/events', (req, res) => { - if (typeof req.body.appId === "undefined") { - // TODO: Use some kind of authentication for this? - res.status(400).json({ message: 'Invalid appId' }); - return; - } - - if (typeof req.body.sessionId === "undefined") { - res.status(400).json({ message: 'Invalid sessionId' }); - return; - } - - if (Event.types.indexOf(req.body.type) === -1) { - res.status(400).json({ message: 'Invalid event type' }); - return; - } - - if (typeof req.body.data === "undefined") { - // TODO: Handle data validation better than this - res.status(400).json({ message: 'Invalid data' }); - return; - } - - const event = new Event( - req.body.appId, - req.body.date, - req.body.userAgent, - req.body.platform, - req.body.manufacturer, - req.body.model, - req.body.version, - req.body.locale, - req.body.sessionId, - req.body.data, - req.body.type, - ); - - events.push(event); - res.json(event); -}); - -const port = process.env.PORT || 3000; +app.use('/apps', appRouter) +app.use('/events', eventRouter) app.listen(port, () => { console.log(`Started Flayre server on port ${port}`); -}); \ No newline at end of file +}); diff --git a/package-lock.json b/package-lock.json index 5dfbf3d..6475252 100644 --- a/package-lock.json +++ b/package-lock.json @@ -147,6 +147,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, "bcrypt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz", @@ -591,6 +599,14 @@ "vary": "~1.1.2" } }, + "express-basic-auth": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.0.tgz", + "integrity": "sha512-iJ0h1Gk6fZRrFmO7tP9nIbxwNgCUJASfNj5fb0Hy15lGtbqqsxpt7609+wq+0XlByZjXmC/rslWQtnuSTVRIcg==", + "requires": { + "basic-auth": "^2.0.1" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 2b81551..6708e93 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "bcrypt": "^5.0.0", "express": "^4.17.1", + "express-basic-auth": "^1.2.0", "mysql": "^2.18.1" }, "devDependencies": {