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