Standardise CheckMK variables to the cmk prefix (checkMkUrl -> cmkUrl, checkmk* -> cmk*) and resolve the ansible/semaphore split by renaming all booking fields to semaphore*. Includes DB migrations 0001/0002 for existing databases.
208 lines
7.1 KiB
TypeScript
208 lines
7.1 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import path from 'path';
|
|
|
|
export const DB_FILE = path.join(process.cwd(), 'ghostgrid.db');
|
|
|
|
/** App-generated primary key: `${prefix}-${epochMs}-${rand}` (e.g. `log-…`, `dev-…`). */
|
|
export const uid = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
|
|
console.log(`[Database] Connecting to SQLite database at: ${DB_FILE}`);
|
|
const db = new Database(DB_FILE);
|
|
|
|
db.pragma('journal_mode = WAL');
|
|
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'User',
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS devices (
|
|
id TEXT PRIMARY KEY,
|
|
hostname TEXT NOT NULL,
|
|
ip TEXT NOT NULL,
|
|
location TEXT NOT NULL,
|
|
notes TEXT,
|
|
type TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
emergencySheet TEXT NOT NULL,
|
|
lastCheckedAt TEXT,
|
|
cmkUrl TEXT NOT NULL DEFAULT '',
|
|
cmkHostname TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS labs (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT NOT NULL,
|
|
contactPerson TEXT NOT NULL,
|
|
location TEXT NOT NULL,
|
|
deviceIds TEXT NOT NULL,
|
|
topology TEXT NOT NULL,
|
|
semaphoreSetupTemplateId TEXT NOT NULL DEFAULT '',
|
|
semaphoreTeardownTemplateId TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS bookings (
|
|
id TEXT PRIMARY KEY,
|
|
labId TEXT NOT NULL,
|
|
userId TEXT NOT NULL,
|
|
startDateTime TEXT NOT NULL,
|
|
endDateTime TEXT NOT NULL,
|
|
notes TEXT,
|
|
status TEXT NOT NULL,
|
|
notified INTEGER NOT NULL DEFAULT 0,
|
|
emailSent INTEGER NOT NULL DEFAULT 0,
|
|
semaphoreSetupTriggered INTEGER NOT NULL DEFAULT 0,
|
|
semaphoreTeardownTriggered INTEGER NOT NULL DEFAULT 0,
|
|
semaphoreSetupJobId TEXT NOT NULL DEFAULT '',
|
|
semaphoreTeardownJobId TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS logs (
|
|
id TEXT PRIMARY KEY,
|
|
timestamp TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
deviceId TEXT,
|
|
userId TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS links (
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
url TEXT NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
category TEXT NOT NULL DEFAULT '',
|
|
color TEXT NOT NULL DEFAULT 'emerald',
|
|
createdBy TEXT,
|
|
createdAt TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS caddy (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
hostname TEXT NOT NULL,
|
|
upstream TEXT NOT NULL,
|
|
tls INTEGER NOT NULL DEFAULT 1,
|
|
compress INTEGER NOT NULL DEFAULT 1,
|
|
redirect TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
|
|
// Idempotent migrations — SQLite throws on duplicate column; the catch makes them safe to re-run.
|
|
try { db.prepare("ALTER TABLE labs ADD COLUMN scope TEXT NOT NULL DEFAULT 'global'").run(); } catch {}
|
|
try { db.prepare("ALTER TABLE labs ADD COLUMN ownerId TEXT NOT NULL DEFAULT ''").run(); } catch {}
|
|
|
|
// Seed default settings — INSERT OR IGNORE writes a key only if it is absent.
|
|
const DEFAULT_SETTINGS: Record<string, string> = {
|
|
azure_enabled: 'false',
|
|
azure_client_id: '',
|
|
azure_tenant_id: '',
|
|
azure_client_secret: '',
|
|
azure_redirect_uri: '',
|
|
azure_allowed_group: '',
|
|
checkmk_enabled: 'false',
|
|
checkmk_api_url: '',
|
|
checkmk_api_user: 'automation',
|
|
checkmk_api_secret: '',
|
|
checkmk_sync_interval_ms: '60000',
|
|
semaphore_enabled: 'false',
|
|
semaphore_api_url: '',
|
|
semaphore_api_token: '',
|
|
semaphore_project_id: '',
|
|
caddy_enabled: 'false',
|
|
caddy_admin_url: 'http://127.0.0.1:2019',
|
|
};
|
|
|
|
const seedSetting = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
|
|
for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) seedSetting.run(key, value);
|
|
|
|
export function getSetting(key: string): string {
|
|
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
|
|
return row?.value ?? '';
|
|
}
|
|
|
|
export function setSetting(key: string, value: string): void {
|
|
db.prepare("INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now'))").run(key, value);
|
|
}
|
|
|
|
export function getAllSettings(): Record<string, string> {
|
|
const rows = db.prepare('SELECT key, value FROM settings').all() as { key: string; value: string }[];
|
|
return Object.fromEntries(rows.map(r => [r.key, r.value]));
|
|
}
|
|
|
|
const insertLog = db.prepare(
|
|
'INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)',
|
|
);
|
|
|
|
/**
|
|
* Append a logbook entry. `deviceId`/`userId` default to NULL; `timestamp`
|
|
* defaults to now (pass one to share a single timestamp across a batch).
|
|
* Returns the generated log id.
|
|
*/
|
|
export function addLog(
|
|
type: string,
|
|
message: string,
|
|
opts: { deviceId?: string | null; userId?: string | null; timestamp?: string } = {},
|
|
): string {
|
|
const id = uid('log');
|
|
insertLog.run(id, opts.timestamp ?? new Date().toISOString(), type, message, opts.deviceId ?? null, opts.userId ?? null);
|
|
return id;
|
|
}
|
|
|
|
/** A reverse-proxy route as stored (booleans are SQLite 0/1 integers). */
|
|
export interface CaddyRoute {
|
|
id: number;
|
|
hostname: string;
|
|
upstream: string;
|
|
tls: number;
|
|
compress: number;
|
|
redirect: string;
|
|
created_at: string;
|
|
}
|
|
|
|
/** Fields a caller supplies to create or update a route (JS booleans). */
|
|
export interface CaddyRouteInput {
|
|
hostname: string;
|
|
upstream: string;
|
|
tls: boolean;
|
|
compress: boolean;
|
|
redirect?: string;
|
|
}
|
|
|
|
export function getCaddyRoutes(): CaddyRoute[] {
|
|
return db.prepare('SELECT * FROM caddy ORDER BY id ASC').all() as CaddyRoute[];
|
|
}
|
|
|
|
export function getCaddyRouteById(id: number): CaddyRoute | undefined {
|
|
return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute | undefined;
|
|
}
|
|
|
|
export function addCaddyRoute(route: CaddyRouteInput): CaddyRoute {
|
|
const { lastInsertRowid } = db.prepare(
|
|
'INSERT INTO caddy (hostname, upstream, tls, compress, redirect) VALUES (?, ?, ?, ?, ?)',
|
|
).run(route.hostname, route.upstream, route.tls ? 1 : 0, route.compress ? 1 : 0, route.redirect ?? '');
|
|
return getCaddyRouteById(Number(lastInsertRowid))!;
|
|
}
|
|
|
|
export function updateCaddyRoute(id: number, route: CaddyRouteInput): CaddyRoute {
|
|
db.prepare('UPDATE caddy SET hostname = ?, upstream = ?, tls = ?, compress = ?, redirect = ? WHERE id = ?')
|
|
.run(route.hostname, route.upstream, route.tls ? 1 : 0, route.compress ? 1 : 0, route.redirect ?? '', id);
|
|
return getCaddyRouteById(id)!;
|
|
}
|
|
|
|
export function deleteCaddyRoute(id: number): void {
|
|
db.prepare('DELETE FROM caddy WHERE id = ?').run(id);
|
|
}
|
|
|
|
export default db; |