/** @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 && (
)}
{isProd ? (
{assets?.css.map((css, i) => (
))}
) : (