/** @jsxImportSource hono/jsx */ import type { App } from "App"; import { isDebug } from "core/env"; import { config } from "core/config"; import { $console } from "bknd/utils"; 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"; import type { AppTheme } from "ui/client/use-theme"; import type { Manifest } from "vite"; const htmlBkndContextReplace = ""; export type AdminBkndWindowContext = { user?: TApiUser; logout_route: string; admin_basepath: string; logo_return_path?: string; theme?: AppTheme; }; // @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?: AppTheme; logoReturnPath?: string; manifest?: Manifest; }; 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 are not allowed to read the schema", "warning"); }, context: (c) => ({}), }), async (c) => { const obj: AdminBkndWindowContext = { user: c.get("auth")?.user, logout_route: authRoutes.logout, admin_basepath: this.options.adminBasepath.replace(/\/+$/, ""), 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 options = { 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); } }, context: (c) => ({}), }; const redirectRouteParams = [ permission(SystemPermissions.accessAdmin, options as any), permission(SystemPermissions.schemaRead, options), 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: Manifest; if (this.options.manifest) { manifest = this.options.manifest; } else if (this.options.assetsPath.startsWith("http")) { manifest = await fetch(this.options.assetsPath + ".vite/manifest.json", { headers: { Accept: "application/json", }, }).then((res) => res.json()); } else { // @ts-ignore manifest = await import("bknd/dist/manifest.json", { with: { type: "json" }, }).then((res) => res.default); } try { const entry = Object.values(manifest).find((m) => m.isEntry); if (!entry) { throw new Error("No entry found in manifest"); } assets.js = entry?.file; assets.css = entry?.css ?? []; } 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 && (