Initial commit

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2020-08-30 13:42:50 -07:00
commit a16ff6be7a
11 changed files with 2039 additions and 0 deletions

29
.eslintrc.js Normal file
View file

@ -0,0 +1,29 @@
module.exports = {
'env': {
'node': true,
'es2020': true
},
'extends': 'eslint:recommended',
'parserOptions': {
'ecmaVersion': 12,
'impliedStrict': true
},
'rules': {
'indent': [
'error',
4
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
]
}
};

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules/

17
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/index.js"
}
]
}

7
app.js Normal file
View file

@ -0,0 +1,7 @@
import { randomId } from './util.js';
export default class App {
id = randomId(32);
name = '';
users = [];
}

71
db.js Normal file
View file

@ -0,0 +1,71 @@
import App from './app.js';
import Event from './event.js';
import mysql from 'mysql';
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'
});
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 JSON DEFAULT NULL,
element VARCHAR(256) DEFAULT NULL,
type VARCHAR(256) DEFAULT NULL,
stacktrace VARCHAR(2048) DEFAULT NULL,
fatal BOOLEAN DEFAULT NULL
)`);
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);
});
});
}
}

125
event.js Normal file
View file

@ -0,0 +1,125 @@
import { randomId } from './util.js';
export default class Event {
id = randomId(32);
appId = '';
date = new Date();
// For web only
userAgent = '';
platform = '';
// Unused on web
manufacturer = '';
// This doubles as the browser for web
model = '';
version = '';
locale = '';
sessionId = '';
data;
// For interactions only
// The path for page views or some identifier for clicks
element;
// view or click, more could be added later
type;
// For errors only
stacktrace;
fatal;
constructor(
appId,
date,
userAgent,
platform,
manufacturer,
model,
version,
locale,
sessionId,
data,
element,
type,
stacktrace,
fatal
) {
this.appId = appId;
this.date = date;
this.userAgent = userAgent;
this.platform = platform;
this.manufacturer = manufacturer;
this.model = model;
this.version = version;
this.locale = locale;
this.sessionId = sessionId;
this.data = data;
this.element = element;
this.type = type;
this.stacktrace = stacktrace;
this.fatal = fatal;
}
static Interaction(
appId,
date,
userAgent,
platform,
manufacturer,
model,
version,
locale,
sessionId,
data,
element,
type,
) {
return new Event(
appId,
date,
userAgent,
platform,
manufacturer,
model,
version,
locale,
sessionId,
data,
element,
type,
)
}
static Error(
appId,
date,
userAgent,
platform,
manufacturer,
model,
version,
locale,
sessionId,
data,
stacktrace,
fatal
) {
return new Event(
appId,
date,
userAgent,
platform,
manufacturer,
model,
version,
locale,
sessionId,
data,
null,
null,
stacktrace,
fatal,
)
}
}

89
index.js Normal file
View file

@ -0,0 +1,89 @@
import express from 'express';
import Event from './event.js';
import { randomId, firstOfMonth, lastOfMonth } from './util.js';
import { EventRepository } from './db.js';
const app = express();
app.use(express.json());
let events = [];
app.get('/', (req, res) => {
res.send('Hello, world!');
});
app.get('/id', (req, res) => {
const length = Number.parseInt(req.query['length']) || 32;
res.send(randomId(length));
});
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.sessionId === "undefined") {
res.status(400).json({ message: 'Invalid sessionId' });
return;
}
let event;
if (typeof req.body.element === "string"
&& typeof req.body.type === "string") {
if (req.body.type !== 'view' || req.body.type !== 'click') {
res.status(400).json({ message: 'Invalid event type' });
return;
}
event = Event.Interaction(
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.element,
req.body.type,
);
} else if (typeof req.body.stacktrace === "string"
&& typeof req.body.fatal === "boolean") {
event = Event.Error(
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.element,
req.body.type,
);
} else {
res.status(400).json({ message: 'Invalid event data' });
return;
}
events.push(event);
res.json(event);
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Started Flayre server on port ${port}`);
});

1648
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

21
package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "flayre-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "William Brawner <me@wbrawner.com>",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.0.0",
"express": "^4.17.1",
"mysql": "^2.18.1"
},
"devDependencies": {
"eslint": "^7.7.0"
},
"type": "module"
}

8
user.js Normal file
View file

@ -0,0 +1,8 @@
import { randomId } from './util.js';
export default class User {
id = randomId(8);
name = '';
email = '';
password = '';
}

23
util.js Normal file
View file

@ -0,0 +1,23 @@
export function randomId(length) {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
let id = ""
while (id.length < length) {
id += characters[Math.floor(Math.random() * characters.length)]
}
return id
}
export function firstOfMonth() {
const d = new Date();
d.setUTCDate(1);
d.setUTCHours(0, 0, 0, 0);
return d;
}
export function lastOfMonth() {
const d = new Date();
d.setUTCMonth(d.getUTCMonth() + 1)
d.setUTCDate(0);
d.setUTCHours(23, 59, 59, 999);
return d;
}