/** @jsxImportSource hono/jsx */ import type { App } from "App"; import { type ClassController, isDebug } from "core"; import { addFlashMessage } from "core/server/flash"; import { Hono } from "hono"; import { html } from "hono/html"; import { Fragment } from "hono/jsx"; import * as SystemPermissions from "modules/permissions"; const htmlBkndContextReplace = ""; // @todo: add migration to remove admin path from config export type AdminControllerOptions = { basepath?: string; html?: string; forceDev?: boolean | { mainPath: string }; }; export class AdminController implements ClassController { constructor( private readonly app: App, private options: AdminControllerOptions = {} ) {} get ctx() { return this.app.modules.ctx(); } get basepath() { return this.options.basepath ?? "/"; } private withBasePath(route: string = "") { return (this.basepath + route).replace(/\/+$/, "/"); } getController(): Hono { const auth = this.app.module.auth; const configs = this.app.modules.configs(); // if auth is not enabled, authenticator is undefined const auth_enabled = configs.auth.enabled; const hono = new Hono<{ Variables: { html: string; }; }>().basePath(this.withBasePath()); const authRoutes = { root: "/", success: configs.auth.cookie.pathSuccess ?? "/", loggedOut: configs.auth.cookie.pathLoggedOut ?? "/", login: "/auth/login", logout: "/auth/logout" }; hono.use("*", async (c, next) => { const obj = { user: auth.authenticator?.getUser(), logout_route: this.withBasePath(authRoutes.logout) }; const html = await this.getHtml(obj); if (!html) { console.warn("Couldn't generate HTML for admin UI"); // re-casting to void as a return is not required return c.notFound() as unknown as void; } c.set("html", html); // refresh cookie if needed await auth.authenticator?.requestCookieRefresh(c); await next(); }); if (auth_enabled) { hono.get(authRoutes.login, async (c) => { if ( this.app.module.auth.authenticator?.isUserLoggedIn() && this.ctx.guard.granted(SystemPermissions.accessAdmin) ) { return c.redirect(authRoutes.success); } const html = c.get("html"); return c.html(html); }); hono.get(authRoutes.logout, async (c) => { await auth.authenticator?.logout(c); return c.redirect(authRoutes.loggedOut); }); } hono.get("*", async (c) => { if (!this.ctx.guard.granted(SystemPermissions.accessAdmin)) { await addFlashMessage(c, "You are not authorized to access the Admin UI", "error"); return c.redirect(authRoutes.login); } const html = c.get("html"); return c.html(html); }); return hono; } private async getHtml(obj: any = {}) { const bknd_context = `window.__BKND__ = JSON.parse('${JSON.stringify(obj)}');`; if (this.options.html) { if (this.options.html.includes(htmlBkndContextReplace)) { return this.options.html.replace( htmlBkndContextReplace, "" ); } console.warn( `Custom HTML needs to include '${htmlBkndContextReplace}' to inject BKND context` ); return this.options.html as string; } const configs = this.app.modules.configs(); const isProd = !isDebug() && !this.options.forceDev; const mainPath = typeof this.options.forceDev === "object" && "mainPath" in this.options.forceDev ? this.options.forceDev.mainPath : "/src/ui/main.tsx"; const assets = { js: "main.js", css: "styles.css" }; if (isProd) { try { // @ts-ignore const manifest = await import("bknd/dist/manifest.json", { assert: { type: "json" } }).then((m) => m.default); assets.js = manifest["src/ui/main.tsx"].name; assets.css = manifest["src/ui/main.css"].name; } catch (e) { console.error("Error loading manifest", e); } } return ( {/* dnd complains otherwise */} {html``} BKND {isProd ? (