Declutter index.js and add basic auth to protect some routes

This commit is contained in:
William Brawner 2020-09-01 16:13:40 +00:00
parent 6fc06365c6
commit 7b742d4750
7 changed files with 313 additions and 272 deletions

145
app.js
View file

@ -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;
}
}
}
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();
})
})

16
config.js Normal file
View file

@ -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;

145
db.js
View file

@ -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);
});
});
}
}
export default pool

127
event.js
View file

@ -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);
});

135
index.js
View file

@ -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}`);
});
});

16
package-lock.json generated
View file

@ -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",

View file

@ -12,6 +12,7 @@
"dependencies": {
"bcrypt": "^5.0.0",
"express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"mysql": "^2.18.1"
},
"devDependencies": {