mirror of
https://github.com/arthur-pbty/flint.git
synced 2026-08-01 20:29:03 +02:00
feat: introduce web dashboard with multi-bot management
- add Discord OAuth2 authentication - allow users to register their bots via token - implement dynamic bot start/stop system - store bots in database (multi-tenant) - replace single-bot env setup with scalable architecture
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@saas/shared",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const BOT_CONTROL_QUEUE = "bot-control";
|
||||
export const BOT_CONTROL_QUEUE_PREFIX = "discord-saas";
|
||||
|
||||
export const BOT_STATUS = ["stopped", "starting", "running", "stopping", "error"] as const;
|
||||
export type BotStatus = (typeof BOT_STATUS)[number];
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./constants.js";
|
||||
export * from "./redisKeys.js";
|
||||
export * from "./tokenCrypto.js";
|
||||
export * from "./types.js";
|
||||
@@ -0,0 +1,16 @@
|
||||
const sanitizeSegment = (segment: string): string => segment.replace(/[^a-zA-Z0-9:_-]/g, "_");
|
||||
|
||||
export const tenantRedisKey = (tenantId: string, ...parts: string[]): string => {
|
||||
const tail = parts.map(sanitizeSegment).join(":");
|
||||
return tail.length > 0
|
||||
? `tenant:${sanitizeSegment(tenantId)}:${tail}`
|
||||
: `tenant:${sanitizeSegment(tenantId)}`;
|
||||
};
|
||||
|
||||
export const tenantRateLimitKey = (tenantId: string, scope: string): string => {
|
||||
return tenantRedisKey(tenantId, "ratelimit", scope);
|
||||
};
|
||||
|
||||
export const tenantBotRedisKey = (tenantId: string, botId: string, ...parts: string[]): string => {
|
||||
return tenantRedisKey(tenantId, "bot", botId, ...parts);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
||||
|
||||
const ALGORITHM = "aes-256-gcm";
|
||||
const IV_LENGTH = 12;
|
||||
|
||||
export interface EncryptedToken {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export const parseTokenEncryptionKey = (base64Key: string): Buffer => {
|
||||
const key = Buffer.from(base64Key, "base64");
|
||||
if (key.length !== 32) {
|
||||
throw new Error("TOKEN_ENCRYPTION_KEY must decode to exactly 32 bytes");
|
||||
}
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
export const encryptToken = (plainToken: string, key: Buffer): EncryptedToken => {
|
||||
const iv = randomBytes(IV_LENGTH);
|
||||
const cipher = createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const ciphertext = Buffer.concat([cipher.update(plainToken, "utf8"), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return {
|
||||
ciphertext: ciphertext.toString("base64"),
|
||||
iv: iv.toString("base64"),
|
||||
tag: tag.toString("base64"),
|
||||
};
|
||||
};
|
||||
|
||||
export const decryptToken = (encryptedToken: EncryptedToken, key: Buffer): string => {
|
||||
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(encryptedToken.iv, "base64"));
|
||||
decipher.setAuthTag(Buffer.from(encryptedToken.tag, "base64"));
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(Buffer.from(encryptedToken.ciphertext, "base64")),
|
||||
decipher.final(),
|
||||
]);
|
||||
|
||||
return decrypted.toString("utf8");
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { BotStatus } from "./constants.js";
|
||||
|
||||
export interface TenantRecord {
|
||||
id: string;
|
||||
ownerUserId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface UserRecord {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
discordUserId: string;
|
||||
username: string;
|
||||
avatarUrl: string | null;
|
||||
role: "owner" | "member";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BotRecord {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
ownerUserId: string;
|
||||
discordBotId: string;
|
||||
displayName: string;
|
||||
tokenCiphertext: string;
|
||||
tokenIv: string;
|
||||
tokenTag: string;
|
||||
status: BotStatus;
|
||||
lastError: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PublicBot {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
discordBotId: string;
|
||||
displayName: string;
|
||||
status: BotStatus;
|
||||
lastError: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type BotControlAction = "start" | "stop" | "restart";
|
||||
|
||||
export interface BotControlJob {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
botId: string;
|
||||
action: BotControlAction;
|
||||
requestedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user