Declutter index.js and add basic auth to protect some routes
This commit is contained in:
parent
6fc06365c6
commit
7b742d4750
7 changed files with 313 additions and 272 deletions
145
app.js
145
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
16
config.js
Normal 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
145
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
export default pool
|
||||
|
|
127
event.js
127
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);
|
||||
});
|
||||
|
|
135
index.js
135
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}`);
|
||||
});
|
||||
});
|
||||
|
|
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"dependencies": {
|
||||
"bcrypt": "^5.0.0",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"mysql": "^2.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
Loading…
Reference in a new issue