BetterFrame/server/src/plugins/service-admin-http/index.ts

172 lines
5.3 KiB
TypeScript
Raw Normal View History

2026-05-09 23:09:13 +00:00
/**
* service-admin-http h3 listener for admin UI and admin API.
2026-05-09 23:09:13 +00:00
*
* Port 18080 behind Angie proxy. Initializes secrets + auth as
* shared modules (not BSB plugins).
2026-05-09 23:09:13 +00:00
*/
import * as av from "@anyvali/js";
import {
BSBService,
type BSBServiceConstructor,
createConfigSchema,
createEventSchemas,
type Observable,
} from "@bsb/base";
import { H3, serve } from "h3";
import type { Server } from "srvx";
import { getRepo } from "../../shared/plugin-registry.js";
import { initSecrets, type SecretsApi } from "../../shared/secrets.js";
import { createAuth, type AuthApi } from "../../shared/auth.js";
import type { Repository } from "../service-store/repository.js";
2026-05-09 23:09:13 +00:00
import { registerMiddleware } from "./middleware.js";
import { registerSetupRoutes } from "./routes-setup.js";
import { registerAuthRoutes } from "./routes-auth.js";
import { registerAdminRoutes } from "./routes-admin.js";
import { registerAccountRoutes } from "./routes-account.js";
import { registerStaticRoutes } from "./routes-static.js";
// ---- Config -----------------------------------------------------------------
const ConfigSchema = av.object(
{
host: av.string().default("127.0.0.1"),
port: av.int().min(1).max(65535).default(18080),
// Secrets config (was service-secrets)
dataDir: av.string().minLength(1).default("/var/lib/betterframe"),
systemdCredsName: av.string().default("betterframe-secret"),
// Auth config (was service-auth)
sessionIdleSeconds: av.int().min(60).default(43200),
sessionMaxSeconds: av.int().min(3600).default(2592000),
loginLockoutThreshold: av.int().min(1).default(8),
loginLockoutSeconds: av.int().min(1).default(900),
argon2Memory: av.int().min(8).default(65536),
argon2TimeCost: av.int().min(1).default(3),
argon2Parallelism: av.int().min(1).default(2),
totpIssuer: av.string().minLength(1).default("BetterFrame"),
cookieName: av.string().minLength(1).default("betterframe_session"),
2026-05-09 23:09:13 +00:00
},
{ unknownKeys: "strip" },
);
export const Config = createConfigSchema(
{
name: "service-admin-http",
description: "h3 HTTP server for admin UI and admin API endpoints.",
tags: ["service", "http", "admin"],
},
ConfigSchema,
);
export const EventSchemas = createEventSchemas({
emitEvents: {},
onEvents: {},
emitReturnableEvents: {},
onReturnableEvents: {},
emitBroadcast: {},
onBroadcast: {},
});
// ---- Deps interface shared with route modules -------------------------------
export interface AdminDeps {
repo: Repository;
auth: AuthApi;
secrets: SecretsApi;
2026-05-09 23:09:13 +00:00
cookieName: string;
}
// ---- Plugin -----------------------------------------------------------------
export class Plugin extends BSBService<InstanceType<typeof Config>, typeof EventSchemas> {
static override Config = Config;
static override EventSchemas = EventSchemas;
initBeforePlugins?: string[];
initAfterPlugins?: string[] = ["service-store"];
2026-05-09 23:09:13 +00:00
runBeforePlugins?: string[];
runAfterPlugins?: string[];
private server?: Server;
constructor(cfg: BSBServiceConstructor<InstanceType<typeof Config>, typeof EventSchemas>) {
super(cfg);
}
async init(obs: Observable): Promise<void> {
// Init shared modules — no inter-plugin wiring needed
const repo = getRepo();
const secrets = initSecrets(
{ dataDir: this.config.dataDir, systemdCredsName: this.config.systemdCredsName },
{ info: (m) => obs.log.info(m as any, {}), warn: (m) => obs.log.warn(m as any, {}) },
);
const auth = createAuth(repo, secrets, {
sessionIdleSeconds: this.config.sessionIdleSeconds,
sessionMaxSeconds: this.config.sessionMaxSeconds,
loginLockoutThreshold: this.config.loginLockoutThreshold,
loginLockoutSeconds: this.config.loginLockoutSeconds,
argon2Memory: this.config.argon2Memory,
argon2TimeCost: this.config.argon2TimeCost,
argon2Parallelism: this.config.argon2Parallelism,
totpIssuer: this.config.totpIssuer,
cookieName: this.config.cookieName,
});
2026-05-09 23:09:13 +00:00
const deps: AdminDeps = {
repo,
auth,
secrets,
cookieName: this.config.cookieName,
2026-05-09 23:09:13 +00:00
};
const app = new H3();
2026-05-09 23:09:13 +00:00
registerMiddleware(app, deps);
registerStaticRoutes(app);
registerSetupRoutes(app, deps);
registerAuthRoutes(app, deps);
registerAdminRoutes(app, deps);
registerAccountRoutes(app, deps);
app.get("/healthz", () => ({ status: "ok" }));
app.get("/readyz", () => {
try {
deps.repo.isSetupComplete();
2026-05-09 23:09:13 +00:00
return { status: "ready" };
} catch {
return { status: "not_ready" };
}
});
app.get("/version", () => ({
name: "betterframe",
version: "0.1.0",
now: new Date().toISOString(),
}));
app.get("/", () => {
if (!deps.repo.isSetupComplete()) {
2026-05-09 23:09:13 +00:00
return new Response(null, { status: 302, headers: { location: "/setup" } });
}
return new Response(null, { status: 302, headers: { location: "/admin/" } });
});
this.server = serve(app, {
port: this.config.port,
hostname: this.config.host,
});
obs.log.info("admin-http listening on {host}:{port}", {
host: this.config.host,
port: this.config.port,
});
}
async run(_obs: Observable): Promise<void> {}
async dispose(): Promise<void> {
if (this.server) {
await this.server.close();
}
}
}