/** @jsxImportSource hono/jsx */ import type { App } from "App"; import { $console, config, isDebug } from "core"; import { addFlashMessage } from "core/server/flash"; import { html } from "hono/html"; import { Fragment } from "hono/jsx"; import { css, Style } from "hono/css"; import { Controller } from "modules/Controller"; import * as SystemPermissions from "modules/permissions"; import type { TApiUser } from "Api"; const htmlBkndContextReplace = ""; export type AdminBkndWindowContext = { user?: TApiUser; logout_route: string; admin_basepath: string; logo_return_path?: string; theme?: "dark" | "light" | "system"; }; // @todo: add migration to remove admin path from config export type AdminControllerOptions = { basepath?: string; adminBasepath?: string; assetsPath?: string; html?: string; forceDev?: boolean | { mainPath: string }; debugRerenders?: boolean; theme?: "dark" | "light" | "system"; logoReturnPath?: string; }; export class AdminController extends Controller { constructor( private readonly app: App, private _options: AdminControllerOptions = {}, ) { super(); } get ctx() { return this.app.modules.ctx(); } get options() { return { ...this._options, basepath: this._options.basepath ?? "/", adminBasepath: this._options.adminBasepath ?? "", assetsPath: this._options.assetsPath ?? config.server.assets_path, theme: this._options.theme ?? "system", logo_return_path: this._options.logoReturnPath ?? "/", }; } get basepath() { return this.withAdminBasePath(); } private withBasePath(route: string = "") { return (this.options.basepath + route).replace(/(? { if (!path.startsWith("/auth")) { addFlashMessage(c, "You are not authorized to access the Admin UI", "error"); $console.log("redirecting", authRoutes.login); return c.redirect(authRoutes.login); } return; }, }), permission(SystemPermissions.schemaRead, { onDenied: async (c) => { addFlashMessage(c, "You not allowed to read the schema", "warning"); }, }), async (c) => { const obj: AdminBkndWindowContext = { user: c.get("auth")?.user, logout_route: authRoutes.logout, admin_basepath: this.options.adminBasepath, theme: this.options.theme, logo_return_path: this.options.logoReturnPath, }; 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; } await auth.authenticator?.requestCookieRefresh(c); return c.html(html); }, ); } if (auth_enabled) { const redirectRouteParams = [ permission([SystemPermissions.accessAdmin, SystemPermissions.schemaRead], { // @ts-ignore onGranted: async (c) => { // @todo: add strict test to permissions middleware? if (c.get("auth")?.user) { $console.log("redirecting to success"); return c.redirect(authRoutes.success); } }, }), async (c) => { return c.html(c.get("html")!); }, ] as const; hono.get(authRoutes.login, ...redirectRouteParams); hono.get(authRoutes.register, ...redirectRouteParams); hono.get(authRoutes.logout, async (c) => { await auth.authenticator?.logout(c); return c.redirect(authRoutes.loggedOut); }); } return hono; } private async getHtml(obj: AdminBkndWindowContext) { 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 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) { let manifest: any; if (this.options.assetsPath.startsWith("http")) { manifest = await fetch(this.options.assetsPath + "manifest.json", { headers: { Accept: "application/json", }, }).then((res) => res.json()); } else { // @ts-ignore manifest = await import("bknd/dist/manifest.json", { assert: { type: "json" }, }).then((res) => res.default); } try { // @todo: load all marked as entry (incl. css) assets.js = manifest["src/ui/main.tsx"].file; assets.css = manifest["src/ui/main.tsx"].css[0] as any; } catch (e) { $console.warn("Couldn't find assets in manifest", e); } } const favicon = isProd ? this.options.assetsPath + "favicon.ico" : "/favicon.ico"; return ( {/* dnd complains otherwise */} {html``} BKND {this.options.debugRerenders && (