server framework

This commit is contained in:
jusax23 2023-02-28 14:44:10 +01:00
parent 4e5531b2e2
commit 4e74d679cc
17 changed files with 4287 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
config.juml

3298
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "outbag-server",
"version": "0.0.1",
"description": "Did you know that you can host your own outbag instance?",
"main": "dist/main.js",
"type": "module",
"scripts": {
"main": "tsc && node . -c config.juml"
},
"repository": {
"type": "git",
"url": "git+https://codeberg.org/outbag/server.git"
},
"keywords": [
"outbag"
],
"author": "jusax23, comcloudway",
"license": "AGPL-3.0-only",
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^18.11.18",
"@types/pg": "^8.6.6",
"@types/prompts": "^2.4.2",
"esbuild": "^0.17.10",
"pkg": "^5.8.0",
"typescript": "^4.9.4"
},
"dependencies": {
"commander": "^10.0.0",
"dblang": "https://jusax.de/git/attachments/c13552b7-c9f0-4f50-bcce-96a124c1c286",
"express": "^4.18.2",
"juml": "https://jusax.de/git/attachments/208913c5-2851-4b86-a53d-ca99fed168cc",
"nman": "https://jusax.de/git/attachments/5333948b-fe6b-45d2-9230-ca388f6a89bc",
"prompts": "^2.4.2",
"ws": "^8.12.1"
}
}

View file

@ -1,4 +1,6 @@
# Outbag Server
Mybe outdated!!!
Did you know that you can host your own outbag instance?
This repo contains the official outbag server.

55
src/main.ts Normal file
View file

@ -0,0 +1,55 @@
declare global {
var debug: boolean;
}
import express from "express";
import https from "https";
import http from "http";
import { Command } from "commander";
import { oConf } from "./sys/config.js"
import { log } from "./sys/log.js"
import { connectToDB } from "./sys/db.js"
import bruteforce from "./sys/bruteforce.js"
import { getSettings, setSettings } from "./sys/settings.js"
import { fullSetup, partiellSetup } from "./setup/config.js"
const config = {
version: "0.0.1"
};
const program = new Command();
program
.name("Outbag Server")
.description("The default way to host your own Outbag server.")
.version(config.version);
program
.option("-c, --config <path>", "Start the Outbag server with a config file.")
.option("-d, --debug", "Start the Outbag server with more log output.")
.option("-s, --setup", "Start the Server Setup Tool")
.action(({ config, debug, setup }) => {
let dofullSetup = false;
global.debug = debug != null;
if (config) {
log("System", "Starting with config:", config);
if(!oConf.connect(config)) dofullSetup = true;
}
if(setup || dofullSetup){
if(dofullSetup) fullSetup();
else partiellSetup();
}else{
startServer();
}
});
program.parse();
async function startServer() {
await connectToDB();
const server = express();
server.use(bruteforce);
}

58
src/server/outbagURL.ts Normal file
View file

@ -0,0 +1,58 @@
const WELL_KNOWN_PATH = "/.well-known/outbag/server";
const DEFAULT_PORT = 7223;
export const outbagURL = (url: string, prefix = "https") => new Promise((res, rej) => {
let uri: URL;
try {
uri = new URL("/", url);
uri.protocol = "https";
} catch (_) {
uri = new URL("https://" + url);
}
uri.pathname = WELL_KNOWN_PATH;
fetch(uri)
.then(resp => resp.json())
.then(({ path = "/", port = 7223 }) => {
uri.pathname = path;
uri.port = port;
uri.protocol = prefix;
res(uri.toString());
})
.catch(_ => {
if (uri.port == '') {
uri.port = DEFAULT_PORT+"";
outbagURL(uri.toString(), prefix)
.then(url => res(url))
.catch(_ => rej());
} else {
rej();
}
});
});
export const outbagURLshort = (url: string) => new Promise((res, rej) => {
let uri: URL;
try {
uri = new URL("/", url);
uri.protocol = "https";
} catch (_) {
uri = new URL("https://" + url);
}
uri.pathname = WELL_KNOWN_PATH;
fetch(uri)
.then(resp => resp.json())
.then(({ path = "/", port = 7223 }) => {
uri.pathname = path;
uri.port = port;
res(uri.hostname + ":" + uri.port);
})
.catch(_ => {
if (uri.port == '') {
uri.port = DEFAULT_PORT+"";
outbagURLshort(uri.toString())
.then(url => res(url))
.catch(_ => rej());
} else {
rej();
}
});
});

16
src/server/permissions.ts Normal file
View file

@ -0,0 +1,16 @@
export const PERMISSIONS = {
NONE: 0b0000000000000000, // equal to no account or blocked account
DEFAULT: 0b0000000000000011, //default
CAN_USE_API: 0b0000000000000001,
PROVIDE_CERT: 0b0000000000000010,
MANAGE_OTA_TOKENS: 0b0000010000000000,
MANAGE_SERVER_PRODUKT_LIST: 0b0000100000000000,
SHOW_USERS_AND_LISTS: 0b0001000000000000,
EDIT_SETTINGS: 0b0010000000000000,
EDIT_RIGHTS: 0b0100000000000000,
EDIT_USERS: 0b1000000000000000,
ALL: 0b1111111111111111,
};

54
src/server/serverCerts.ts Normal file
View file

@ -0,0 +1,54 @@
import { outbagURL } from "./outbagURL.js";
import { db, serverCerts } from "../sys/db.js"
import { eq, exists, insert, not, select, update } from "dblang";
import { error } from "../sys/log.js";
import { uts } from "../sys/tools.js"
async function updateRemote(url: string, pKey: string = ""): Promise<boolean | string> {
var urlP = await outbagURL(url);
return new Promise((res, rej) => {
fetch(urlP + "api/server/publicKey")
.then(d => d.json())
.then(async json => {
let { publicKey, expires } = json;
if (typeof publicKey == "string" && typeof expires == "string") {
try {
await insert(serverCerts.url, serverCerts.publicKey, serverCerts.expires)
.add(url, publicKey, expires)
.query(db);
} catch (error) {
await update(serverCerts)
.set(serverCerts.publicKey, publicKey)
.set(serverCerts.expires, expires)
.where(eq(serverCerts.url, url))
.query(db);
}
res(publicKey);
} else {
if (publicKey.length > 0) {
res(publicKey);
return;
}
res(false);
}
})
.catch((e) => {
error("serverCert", "fetch error:", e);
if (pKey.length > 0) {
res(pKey);
return;
}
res(false);
})
})
}
export const getRemote = async (url: string) => {
let query = await select([serverCerts.publicKey, serverCerts.expires], serverCerts)
.where(eq(serverCerts.url, url))
.query(db);
if (query.length == 0 || query[0][serverCerts.expires] < uts() - 60)
return await updateRemote(url, query[0][serverCerts.publicKey]);
return query[0][serverCerts.publicKey];
}

335
src/setup/config.ts Normal file
View file

@ -0,0 +1,335 @@
import fs from "fs";
import { shutdown } from "nman";
import prompts from "prompts";
import { oConf } from "../sys/config.js";
import { connectToDB, connectToDBCredentials, db } from "../sys/db.js";
import { log, error, warn } from "../sys/log.js";
const isFile = async (path: string) => {
try {
if (path.startsWith("http")) {
let req = await fetch(path);
return req.status == 200;
} else {
await fs.promises.access(path, fs.constants.F_OK);
let state = await fs.promises.stat(path);
return state.isFile();
}
} catch (error) {
return false;
}
return true;
}
const loading = () => {
var P = ["\\", "|", "/", "-"];
var x = 0;
let intID = setInterval(function () {
process.stdout.write("\r" + P[(x++) % P.length]);
}, 150);
return () => {
process.stdout.write("\r");
clearInterval(intID)
};
};
const system = async () => {
log("Setup", "Setting up generell System Settings:");
const resp = await prompts([
{
type: "number",
name: "port",
message: "Please enter the Port the Server should listen on!",
initial: oConf.get("System", "PORT")
}, {
type: "number",
name: "portexposed",
message: "Please enter the Port on which the server is exposed!",
initial: oConf.get("System", "PORTexposed")
}, {
type: "text",
name: "pathexposed",
message: "Please enter the Path on which the server is exposed!",
initial: oConf.get("System", "PATHexposed")
}, {
type: "text",
name: "url",
message: "Please enter the host name of the server!",
initial: oConf.get("System", "URL")
}, {
type: "number",
name: "certlivesec",
message: "Please enter the number of days after the Server Certificate should be expire!",
initial: oConf.get("System", "CertLiveSec") / 60 / 60 / 24
}
]);
if (
resp.port == null
|| resp.portexposed == null
|| resp.pathexposed == null
|| resp.url == null
|| resp.certlivesec == null
) {
warn("Setup", "You have to provide all informations!");
return false;
};
const resp2 = await prompts({
type: "confirm",
name: "correct",
message: "Are the Details correct?",
initial: true
});
if (!resp2.correct) {
return false;
}
oConf.set("System", "PORT", resp.port);
oConf.set("System", "PORTexposed", resp.portexposed);
oConf.set("System", "PATHexposed", resp.pathexposed);
oConf.set("System", "URL", resp.url);
oConf.set("System", "CertLiveSec", Math.round(resp.certlivesec * 24 * 60 * 60));
oConf.save();
return true;
}
const ssl = async () => {
log("Setup", "Setting up ssl server:");
const respActive = await prompts({
type: "confirm",
name: "ssl",
message: "Activate SSL? (If deactivated Server will run in test mode!)",
initial: true
});
if (respActive.ssl == null) return false;
if (respActive.ssl == false) {
oConf.set("ssl", "enable", false);
}
const resp = await prompts([
{
type: "text",
name: "privkey",
message: "Please enter path or link to privkey.pem",
validate: async (path: string) => await isFile(path) ? true : "Please enter a path or a link to a readable File!",
initial: oConf.get("ssl", "privkey")
}, {
type: "text",
name: "cert",
message: "Please enter path or link to cert.pem",
validate: async (path: string) => await isFile(path) ? true : "Please enter a path or a link to a readable File!",
initial: oConf.get("ssl", "cert")
}, {
type: "text",
name: "chain",
message: "Please enter path or link to chain.pem",
validate: async (path: string) => await isFile(path) ? true : "Please enter a path or a link to a readable File!",
initial: oConf.get("ssl", "chain")
}
]);
if (
resp.privkey == null
|| resp.cert == null
|| resp.chain == null
) {
warn("Setup", "You have to provide all informations!");
return false;
};
const resp2 = await prompts({
type: "confirm",
name: "correct",
message: "Are the Details correct?",
initial: true
});
if (!resp2.correct) {
return false;
}
oConf.set("ssl", "enable", true);
oConf.set("ssl", "privkey", resp.privkey);
oConf.set("ssl", "cert", resp.cert);
oConf.set("ssl", "chain", resp.chain);
oConf.save();
return true;
}
const database = async () => {
log("Setup", "Setting up Database:");
const resp = await prompts([
{
type: "text",
name: "host",
message: "Please enter database host!",
initial: oConf.get("Database", "host")
}, {
type: "number",
name: "port",
message: "Please enter database port!",
initial: oConf.get("Database", "port")
}, {
type: "text",
name: "user",
message: "Please enter the database username!",
initial: oConf.get("Database", "user")
}, {
type: "password",
name: "password",
message: "Please enter the database password!",
initial: oConf.get("Database", "password")
}, {
type: "text",
name: "database",
message: "Please enter the Outbag database Name!",
initial: oConf.get("Database", "database")
}
]);
if (resp.host == null || resp.port == null || resp.user == null || resp.password == null || resp.database == null) {
warn("Setup", "You have to provide all informations!");
return false;
}
const resp2 = await prompts({
type: "confirm",
name: "correct",
message: "Are the Details correct?",
initial: true
});
if (!resp2.correct) {
return false;
}
log("Setup", "Trying to connect to database: ");
let cancle = loading();
try {
await connectToDBCredentials(resp.host, resp.port, resp.user, resp.password, resp.database);
cancle();
log("Setup", "Successful connected to Database!");
oConf.set("Database", "host", resp.host);
oConf.set("Database", "port", resp.port);
oConf.set("Database", "user", resp.user);
oConf.set("Database", "password", resp.password);
oConf.set("Database", "database", resp.database);
oConf.save();
db.close();
return true;
} catch (e) {
cancle();
error("Setup", "Error while connecting to Database!");
return false;
}
}
const outbag = async () => {
log("Setup", "Setting up generell Outbag Settings:");
const resp = await prompts([
{
type: "number",
name: "maxUsers",
message: "Please enter the maximum amount of server users!",
initial: oConf.get("Settings", "maxUsers"),
validate: i => i >= -1
}, {
type: "number",
name: "defaultMaxRooms",
message: "Please enter the default maximum nuber of rooms!",
initial: oConf.get("Settings", "defaultMaxRooms"),
validate: i => i >= -1
}, {
type: "number",
name: "defaultMaxRoomSize",
message: "Please enter the default maximum amount of Elements in one Room!",
initial: oConf.get("Settings", "defaultMaxRoomSize"),
validate: i => i >= -1
}, {
type: "number",
name: "defaultMaxUsersPerRoom",
message: "Please enter default maximum number of Users per Room!",
initial: oConf.get("Settings", "defaultMaxUsersPerRoom"),
validate: i => i >= -1
},
]);
if (
resp.maxUsers == null
|| resp.defaultMaxRooms == null
|| resp.defaultMaxRoomSize == null
|| resp.defaultMaxUsersPerRoom == null
) {
warn("Setup", "You have to provide all informations!");
return false;
}
const resp2 = await prompts({
type: "confirm",
name: "correct",
message: "Are the Details correct?",
initial: true
});
if (!resp2.correct) {
return false;
}
oConf.set("Settings", "maxUsers", resp.maxUsers);
oConf.set("Settings", "defaultMaxRooms", resp.defaultMaxRooms);
oConf.set("Settings", "defaultMaxRoomSize", resp.defaultMaxRoomSize);
oConf.set("Settings", "defaultMaxUsersPerRoom", resp.defaultMaxUsersPerRoom);
oConf.save();
return true;
}
const setupFunc = async (func: () => Promise<boolean>, name: string) => {
let dbConn = await func();
while (!dbConn) {
console.log();
const resp = await prompts({
type: "confirm",
name: "again",
message: "Try to setup '" + name + "' again?",
initial: false
});
if (!resp.again) break;
dbConn = await func();
}
};
export const partiellSetup = async () => {
while (true) {
log("Setup", "Slection:");
const resp = await prompts({
type: 'select',
name: 'value',
message: 'Which Settings do you want to change?',
choices: [
{ title: 'System', description: 'Setup generell System Settings.' },
{ title: 'SSL', description: 'Setup ssl details.' },
{ title: 'Database', description: 'Setup Database connection.' },
{ title: 'Outbag', description: 'Change generell Outbag Settings.' },
],
initial: 0
});
if (resp.value == 0) await setupFunc(system, 'System');
else if (resp.value == 1) await setupFunc(ssl, 'SSL');
else if (resp.value == 2) await setupFunc(database, 'Database');
else if (resp.value == 3) await setupFunc(outbag, 'Outbag');
else break;
}
await shutdown();
}
export const fullSetup = async () => {
log("Setup", "Starting full setup script:");
await setupFunc(system, 'System');
console.log();
await setupFunc(ssl, 'SSL');
console.log();
await setupFunc(database, 'Database');
console.log();
await setupFunc(outbag, 'Outbag');
log("Setup", "Finished setup script!");
await shutdown();
}

51
src/sys/bruteforce.ts Normal file
View file

@ -0,0 +1,51 @@
import { addShutdownTask } from "nman";
import { log } from "./log.js"
import { uts } from "./tools.js";
import express from "express";
const timeout = 10;
const deleteater = 600;
const maxSus = 100;
var bruteforcedata: { [key: string]: { n: number, t: number } } = {};
export const addBruteforcePotantial = (ip: string) => {
if (bruteforcedata[ip] == null) {
bruteforcedata[ip] = { n: 0, t: uts() };
log("Bruteforce Protection", "add ip: ", ip);
}
bruteforcedata[ip].n++;
bruteforcedata[ip].t = uts();
if (bruteforcedata[ip].n > maxSus) {
log("Bruteforce Protection", "blocking ip: ", ip);
}
}
export const bruteforcecheck = (ip: string) => {
return (bruteforcedata[ip]?.n || 0) <= maxSus || uts() - (bruteforcedata[ip]?.t || uts()) > timeout;
}
var bruteforcedatacleaner = setInterval(async () => {
var utst = uts();
let keys = Object.keys(bruteforcedata);
for (var i = 0; i < keys.length; i++) {
if (utst - bruteforcedata[keys[i]].t > deleteater) {
log("Bruteforce Protection", "remove ip: ", keys[i]);
delete bruteforcedata[keys[i]];
}
}
}, 1000 * 60);
addShutdownTask(() => clearInterval(bruteforcedatacleaner), 5000);
export interface suspectRequest extends express.Request {
suspect?: () => void
}
export default (req: suspectRequest, res: express.Response, next: express.NextFunction) => {
if (!bruteforcecheck(req.ip)) return void res.status(400).send("bruteforce");
req.suspect = () => {
addBruteforcePotantial(req.ip);
};
next();
}

32
src/sys/config.ts Normal file
View file

@ -0,0 +1,32 @@
import juml from "juml";
const conf_struct = {
System: {
PORT: { type: "number", default: 7223, env: "OUTBAG_PORT", comment:"The Server will listen on this Port!" },
PORTexposed: { type: "number", default: 7223, env: "OUTBAG_EXPOSED_PORT" },
PATHexposed: { type: "string", default: "/", env: "OUTBAG_EXPOSED_PATH" },
URL: { type: "string", default: "localhost", env: "OUTBAG_HOST" },
CertLiveSec: { type: "number", default: 60*60*24*30, env: "OUTBAG_CERT_LIVE_SEC" },
},
ssl: {
enable: { type: "boolean", default: false, env: "OUTBAG_SSL_ENABLED" },
privkey: { type: "string", default: "privkey.pem", env: "OUTBAG_SSL_PRIVATE_KEY" },
cert: { type: "string", default: "cert.pem", env: "OUTBAG_SSL_CERT" },
chain: { type: "string", default: "chain.pem", env: "OUTBAG_SSL_CHAIN" }
},
Database: {
host: { type: "string", default: "localhost", env: "OUTBAG_MYSQL_HOST" },
port: { type: "number", default: 3306, env: "OUTBAG_MYSQL_PORT" },
user: { type: "string", default: "admin", env: "OUTBAG_MYSQL_USER" },
password: { type: "string", default: "", env: "OUTBAG_MYSQL_PASSWORD" },
database: { type: "string", default: "outbag", env: "OUTBAG_MYSQL_DATABASE" }
},
Settings: {
maxUsers: { type:"number", default:0, env: "OUTBAG_MAX_USERS"},//Infinity = -1
defaultMaxRooms: { type:"number", default:3, env: "OUTBAG_DEFAULT_MAX_ROOMS"},//Infinity = -1
defaultMaxRoomSize: { type:"number", default:10000, env: "OUTBAG_DEFAULT_MAX_ROOMS_SIZE"},//Infinity = -1
defaultMaxUsersPerRoom: { type:"number", default:5, env: "OUTBAG_DEFAULT_MAX_USERS_PER_ROOM"},//Infinity = -1
}
};
export const oConf = new juml(conf_struct);

57
src/sys/crypto.ts Normal file
View file

@ -0,0 +1,57 @@
import crypto from "crypto";
export const sha256 = (d: any) => crypto.createHash('sha256').update(String(d)).digest('base64');
export const encode = (data: any, type: BufferEncoding = "binary") => {
return new Uint8Array(Buffer.from(data, type));
};
export const decode = (data: any, type: BufferEncoding = "binary") => {
return Buffer.from(data).toString(type);
};
export const generateSigningKey = async () => {
var keyPair = await crypto.webcrypto.subtle.generateKey(
{ name: "RSA-PSS", modulusLength: 4096, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
true,
["sign", "verify"]
);
return {
privateKey: decode(new Uint8Array(await crypto.webcrypto.subtle.exportKey("pkcs8", keyPair.privateKey)), "base64"),
publicKey: decode(new Uint8Array(await crypto.webcrypto.subtle.exportKey("spki", keyPair.publicKey)), "base64")
};
};
export const sign = async (message: string, privateKey: string) => {
var rawdata = encode(message);
var rawkey = await crypto.webcrypto.subtle.importKey("pkcs8", encode(privateKey, "base64"), { name: "RSA-PSS", hash: "SHA-256" }, true, ["sign"]);
var step = await crypto.webcrypto.subtle.sign(
{
name: "RSA-PSS",
saltLength: 32,
},
rawkey,
rawdata
);
return decode(new Uint8Array(step), "base64");
};
export const verify = async (message: string, signature: string, publicKey: string) => {
var rawdata = encode(message);
var rawsig = encode(signature, "base64");
var rawkey = await crypto.webcrypto.subtle.importKey("spki", encode(publicKey, "base64"), { name: "RSA-PSS", hash: "SHA-256" }, true, ["verify"]);
var step = await crypto.webcrypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32,
},
rawkey,
rawsig,
rawdata
);
return step//; ? "valid" : "invalid";
}

179
src/sys/db.ts Normal file
View file

@ -0,0 +1,179 @@
import { BIGINT, BOOL, DB, INT, onAction, SMALLINT, TEXT, TINYINT, VARCHAR } from "dblang"
import { oConf } from "./config.js"
import { PERMISSIONS } from "../server/permissions.js"
import nMan from "nman";
import { log, error } from "./log.js";
export const db = new DB();
export const connectToDB = async () => {
db.connect({
host: oConf.get("Database", "host"),
port: oConf.get("Database", "port"),
user: oConf.get("Database", "user"),
password: oConf.get("Database", "password"),
database: oConf.get("Database", "database"),
});
accounts.deleted.ops.maxRooms = oConf.get("Settings", "defaultMaxRooms");
accounts.deleted.ops.maxRoomSize = oConf.get("Settings", "defaultMaxRoomSize");
accounts.deleted.ops.maxUsersPerRoom = oConf.get("Settings", "defaultMaxUsersPerRoom");
try {
await db.sync(true);
log("Database", "Connected to Database!");
} catch (e) {
error("Database", "Error while conncting to Database!");
throw e;
}
};
export const connectToDBCredentials = async (host: string, port: number, user: string, password: string, database: string) => {
db.connect({
host, port, user, password, database,
});
accounts.deleted.ops.maxRooms = oConf.get("Settings", "defaultMaxRooms");
accounts.deleted.ops.maxRoomSize = oConf.get("Settings", "defaultMaxRoomSize");
accounts.deleted.ops.maxUsersPerRoom = oConf.get("Settings", "defaultMaxUsersPerRoom");
try {
await db.sync(true);
} catch (e) {
throw e;
}
};
nMan.addShutdownTask(db.close, 3000, 10);
export const accounts = db.newTable("accounts");
accounts.addAttributes({
accID: { type: INT, primaryKey: true, autoIncrement: true },
name: { type: VARCHAR(255), default: PERMISSIONS.DEFAULT },
accountKeySalt: { type: VARCHAR(64) },
accountKey: { type: VARCHAR(64) },
viewable: { type: BOOL, default: true },
deleted: { type: BOOL, default: false },
maxRooms: { type: INT },
maxRoomSize: { type: INT },
maxUsersPerRoom: { type: INT },
});
export const settings = db.newTable("settings");
settings.addAttributes({
type: { type: VARCHAR(255), primaryKey: true },
data: { type: TEXT },
})
export const serverCerts = db.newTable("serverCerts");
serverCerts.addAttributes({
serverCertID: { type: INT, primaryKey: true, autoIncrement: true },
url: { type: TEXT },
publicKey: { type: TEXT },
expires: { type: BIGINT }
});
export const signupOTA = db.newTable("signupOTA");
signupOTA.addAttributes({
token: { type: VARCHAR(128), primaryKey: true },
expires: { type: BIGINT },
usesLeft: { type: INT }
});
export const rooms = db.newTable("rooms");
rooms.addAttributes({
roomID: { type: INT, primaryKey: true, autoIncrement: true },
name: { type: VARCHAR(255), unique: true },
owner: {
type: INT,
foreginKey: {
link: accounts.accID
}
},
rights: { type: INT, default: 0b11111 },
public: { type: BOOL, default: 0 },
title: { type: TEXT, default: "" },
description: { type: TEXT, default: "" },
icon: { type: TEXT, default: "" }
});
export const roomMembers = db.newTable("roomMembers");
roomMembers.addAttributes({
roomMemberID: { type: INT, primaryKey: true, autoIncrement: true },
roomID: {
type: INT,
foreginKey: {
link: rooms.roomID,
onDelete: onAction.cascade,
onUpdate: onAction.cascade
}
}
});
export const roomOTAs = db.newTable("roomOTAs");
roomOTAs.addAttributes({
roomID: {
type: INT,
primaryKey: true,
foreginKey: {
link: rooms.roomID,
onDelete: onAction.cascade,
onUpdate: onAction.cascade
}
},
token: {
type: VARCHAR(128),
primaryKey: true
},
expires: { type: BIGINT },
usesLeft: { type: INT }
});
export const listCategories = db.newTable("listCategories");
listCategories.addAttributes({
listCatID: { type: INT, primaryKey: true, autoIncrement: true },
roomID: {
type: INT,
primaryKey: true,
foreginKey: {
link: rooms.roomID,
onDelete: onAction.cascade,
onUpdate: onAction.cascade
}
},
title: { type: TEXT },
weight: { type: INT },
color: { type: TEXT }
});
export const articles = db.newTable("articles");
articles.addAttributes({
articleID: { type: INT, primaryKey: true, autoIncrement: true },
roomID: {
type: INT,
primaryKey: true,
foreginKey: {
link: rooms.roomID,
onDelete: onAction.cascade,
onUpdate: onAction.cascade
}
},
state: { type: TINYINT, default: 0 },
title: { type: TEXT },
description: { type: TEXT },
category: {
type: INT,
foreginKey: {
link: listCategories.listCatID,
onDelete: onAction.setNull,
onUpdate: onAction.cascade
}
},
unit: { type: SMALLINT },
amount: { type: TEXT },
//link: {type:INT} TODO: foreign key
});

41
src/sys/log.ts Normal file
View file

@ -0,0 +1,41 @@
export const debug = (name: string, ...args: any[]) => {
if(!global.debug)return;
console.log(
"\x1b[33m%s\x1b[0m"+
"\x1b[1m\x1b[32m%s\x1b[0m",
(new Date()).toLocaleString(),
` [${name}]:`,
...args
);
};
export const log = (name: string, ...args: string[]) => {
console.log(
"\x1b[33m%s\x1b[0m"+
"\x1b[1m\x1b[36m%s\x1b[0m",
(new Date()).toLocaleString(),
` [${name}]:`,
...args
);
};
export const warn = (name: string, ...args: any[]) => {
console.warn(
"\x1b[33m%s\x1b[0m"+
"\x1b[1m\x1b[36m%s\x1b[0m",
(new Date()).toLocaleString(),
` [${name}]:`,
...args
);
};
export const error = (name: string, ...args: any[]) => {
console.error(
"\x1b[33m%s\x1b[0m"+
"\x1b[1m\x1b[41m%s\x1b[0m\x1b[41m",
(new Date()).toLocaleString()+" ",
`[${name}]:`,
...args,
"\x1b[0m"
);
};

32
src/sys/settings.ts Normal file
View file

@ -0,0 +1,32 @@
import { eq, exists, insert, not, select, update } from "dblang";
import { db, settings } from "./db.js";
export const SETTINGS = {
publicKey: "publicKey",
privateKey: "privateKey",
certExpires: "certExpires"
};
export const getSettings = async (type: string) => {
let query = await select([settings.data], settings)
.where(eq(settings.type, type))
.query(db);
if (!query.length) {
return false;
}
return query[0][settings.data];
};
export const setSettings = async (type: string, data: string) => {
try {
await insert(settings.type, settings.data)
.add(type, data)
.query(db);
} catch (error) {
await update(settings)
.set(settings.data, data)
.where(eq(settings.type, type))
.query(db);
}
};

17
src/sys/tools.ts Normal file
View file

@ -0,0 +1,17 @@
import crypto from "crypto";
export const uts = ()=>{
return Math.floor(new Date().getTime()/1000);
}
const key64 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
export const get64 = (l: number) => {
var out = "";
var val = crypto.webcrypto.getRandomValues(new Uint8Array(l));
for (var i = 0; i < l; i++) {
out += key64[val[i] % key64.length];
}
return out;
};

20
tsconfig.json Normal file
View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"declaration": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"files": [
"src/main.ts"
],
"exclude": [
"node_modules",
"dist"
]
}