a control panel for my server

chore: migrate to sqlite

dunkirk.sh 28e96f2e e5083536

verified
+89 -30
+2 -1
flags.json
··· 5 5 "flags": { 6 6 "block-map-sse": { 7 7 "name": "Block SSE Endpoint", 8 - "description": "Disable /sse Server-Sent Events" 8 + "description": "Disable /sse Server-Sent Events", 9 + "path": "/sse" 9 10 } 10 11 } 11 12 }
+65 -22
src/flags.ts
··· 1 - import { join } from "path"; 2 - import { unlink } from "fs/promises"; 1 + import { Database } from "bun:sqlite"; 3 2 import flagsConfig from "../flags.json"; 4 3 5 - async function exists(path: string): Promise<boolean> { 6 - return Bun.file(path).exists(); 7 - } 4 + const DB_PATH = process.env.DATABASE_PATH || "./data/control.db"; 5 + 6 + // Initialize database 7 + const db = new Database(DB_PATH, { create: true }); 8 + db.exec(` 9 + CREATE TABLE IF NOT EXISTS flags ( 10 + id TEXT PRIMARY KEY, 11 + enabled INTEGER NOT NULL DEFAULT 0, 12 + updated_at TEXT NOT NULL DEFAULT (datetime('now')) 13 + ) 14 + `); 15 + 16 + // Prepared statements for performance 17 + const getFlag = db.prepare<{ enabled: number }, [string]>( 18 + "SELECT enabled FROM flags WHERE id = ?" 19 + ); 20 + const setFlagStmt = db.prepare( 21 + `INSERT INTO flags (id, enabled, updated_at) VALUES (?, ?, datetime('now')) 22 + ON CONFLICT(id) DO UPDATE SET enabled = excluded.enabled, updated_at = datetime('now')` 23 + ); 24 + const getAllFlags = db.prepare<{ id: string; enabled: number }, []>( 25 + "SELECT id, enabled FROM flags" 26 + ); 8 27 9 28 export interface FlagDefinition { 10 29 name: string; 11 30 description: string; 31 + path?: string; // The path this flag blocks (e.g., "/sse") 12 32 } 13 33 14 34 export interface ServiceDefinition { ··· 27 47 enabled: boolean; 28 48 service: string; 29 49 } 30 - 31 - const FLAGS_DIR = process.env.FLAGS_DIR || "/var/lib/caddy/flags"; 32 50 33 51 export function getConfig(): FlagsConfig { 34 52 return flagsConfig as FlagsConfig; ··· 57 75 return null; 58 76 } 59 77 60 - export async function getFlagStatus(flagId: string): Promise<boolean> { 61 - const path = join(FLAGS_DIR, flagId); 62 - return exists(path); 78 + export function getFlagStatus(flagId: string): boolean { 79 + const row = getFlag.get(flagId); 80 + return row?.enabled === 1; 63 81 } 64 82 65 - export async function setFlag(flagId: string, enabled: boolean): Promise<void> { 83 + export function setFlag(flagId: string, enabled: boolean): void { 66 84 if (!getFlagDefinition(flagId)) { 67 85 throw new Error(`Unknown flag: ${flagId}`); 68 86 } 69 - 70 - const path = join(FLAGS_DIR, flagId); 71 - if (enabled) { 72 - await Bun.write(path, ""); 73 - } else { 74 - if (await exists(path)) { 75 - await unlink(path); 76 - } 77 - } 87 + setFlagStmt.run(flagId, enabled ? 1 : 0); 78 88 } 79 89 80 - export async function getAllFlagsStatus(): Promise<Record<string, FlagStatus[]>> { 90 + export function getAllFlagsStatus(): Record<string, FlagStatus[]> { 81 91 const config = getConfig(); 82 92 const result: Record<string, FlagStatus[]> = {}; 83 93 94 + // Get all current flag states from DB 95 + const dbFlags = new Map<string, boolean>(); 96 + for (const row of getAllFlags.all()) { 97 + dbFlags.set(row.id, row.enabled === 1); 98 + } 99 + 84 100 for (const [serviceId, service] of Object.entries(config.services)) { 85 101 const flags: FlagStatus[] = []; 86 102 for (const [flagId, flag] of Object.entries(service.flags)) { ··· 88 104 id: flagId, 89 105 name: flag.name, 90 106 description: flag.description, 91 - enabled: await getFlagStatus(flagId), 107 + enabled: dbFlags.get(flagId) ?? false, 92 108 service: serviceId, 93 109 }); 94 110 } ··· 97 113 98 114 return result; 99 115 } 116 + 117 + // Check if a request should be blocked based on host and path 118 + export function shouldBlock(host: string, path: string): boolean { 119 + const config = getConfig(); 120 + 121 + for (const [serviceId, service] of Object.entries(config.services)) { 122 + // Check if this request matches a service 123 + if (!host.includes(serviceId) && !serviceId.includes(host)) { 124 + continue; 125 + } 126 + 127 + for (const [flagId, flag] of Object.entries(service.flags)) { 128 + // Check if flag is enabled (blocking) 129 + if (!getFlagStatus(flagId)) { 130 + continue; 131 + } 132 + 133 + // Check if the flag applies to this path 134 + const flagPath = flag.path || `/${flagId.split("-").pop()}`; 135 + if (path === flagPath || path.startsWith(flagPath + "/") || path.startsWith(flagPath + "?")) { 136 + return true; 137 + } 138 + } 139 + } 140 + 141 + return false; 142 + }
+22 -7
src/index.ts
··· 18 18 getFlagStatus, 19 19 setFlag, 20 20 getFlagDefinition, 21 + shouldBlock, 21 22 } from "./flags"; 22 23 23 24 import homepage from "../public/index.html"; ··· 35 36 logo_uri: "https://hc-cdn.hel1.your-objectstorage.com/s/v3/d19f900e04238dcd_control.png", 36 37 redirect_uris: [REDIRECT_URI], 37 38 }); 39 + }); 40 + 41 + // Kill-check endpoint for Caddy to call before proxying protected routes 42 + // Returns 200 to allow, 503 to block 43 + // No auth required - this is called by Caddy internally 44 + app.get("/kill-check", (c) => { 45 + const host = c.req.header("X-Orig-Host") || c.req.header("Host") || ""; 46 + const path = c.req.header("X-Orig-Path") || "/"; 47 + 48 + if (shouldBlock(host, path)) { 49 + return c.text("Temporarily disabled", 503); 50 + } 51 + 52 + return c.text("OK", 200); 38 53 }); 39 54 40 55 app.get("/auth/login", (c) => { ··· 99 114 api.use("/flags/*", apiAuthMiddleware); 100 115 api.use("/flags", apiAuthMiddleware); 101 116 102 - api.get("/flags", async (c) => { 103 - const flags = await getAllFlagsStatus(); 117 + api.get("/flags", (c) => { 118 + const flags = getAllFlagsStatus(); 104 119 return c.json(flags); 105 120 }); 106 121 107 - api.get("/flags/:name", async (c) => { 122 + api.get("/flags/:name", (c) => { 108 123 const name = c.req.param("name"); 109 124 const definition = getFlagDefinition(name); 110 125 ··· 112 127 return c.json({ error: "Unknown flag" }, 404); 113 128 } 114 129 115 - const enabled = await getFlagStatus(name); 130 + const enabled = getFlagStatus(name); 116 131 return c.json({ 117 132 id: name, 118 133 name: definition.flag.name, ··· 135 150 return c.json({ error: "Invalid body: enabled must be boolean" }, 400); 136 151 } 137 152 138 - await setFlag(name, body.enabled); 153 + setFlag(name, body.enabled); 139 154 return c.json({ id: name, enabled: body.enabled }); 140 155 }); 141 156 142 - api.delete("/flags/:name", async (c) => { 157 + api.delete("/flags/:name", (c) => { 143 158 const name = c.req.param("name"); 144 159 const definition = getFlagDefinition(name); 145 160 ··· 147 162 return c.json({ error: "Unknown flag" }, 404); 148 163 } 149 164 150 - await setFlag(name, false); 165 + setFlag(name, false); 151 166 return c.json({ id: name, enabled: false }); 152 167 }); 153 168