Initial commit
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
commit
a16ff6be7a
11 changed files with 2039 additions and 0 deletions
29
.eslintrc.js
Normal file
29
.eslintrc.js
Normal 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
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal 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
7
app.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { randomId } from './util.js';
|
||||||
|
|
||||||
|
export default class App {
|
||||||
|
id = randomId(32);
|
||||||
|
name = '';
|
||||||
|
users = [];
|
||||||
|
}
|
71
db.js
Normal file
71
db.js
Normal 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
125
event.js
Normal 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
89
index.js
Normal 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
1648
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
21
package.json
Normal file
21
package.json
Normal 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
8
user.js
Normal 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
23
util.js
Normal 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;
|
||||||
|
}
|
Loading…
Reference in a new issue