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 # Outbag Server
Mybe outdated!!!
Did you know that you can host your own outbag instance? Did you know that you can host your own outbag instance?
This repo contains the official outbag server. 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"
]
}