From 824ff4013339662de82c360535bce5c1d4ac1962 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 23 Nov 2024 18:12:19 +0100 Subject: [PATCH] reworked admin auth to use form and cookie + adjusted oauth to support API and cookie-based auth --- app/src/Api.ts | 43 ++-- app/src/auth/AppAuth.ts | 13 +- app/src/auth/api/AuthController.ts | 22 +- app/src/auth/authenticate/Authenticator.ts | 85 ++++++- .../strategies/PasswordStrategy.ts | 26 +- .../strategies/oauth/OAuthStrategy.ts | 117 ++++++--- app/src/auth/authorize/Guard.ts | 2 +- app/src/core/server/flash.ts | 40 ++++ app/src/modules/ModuleApi.ts | 14 +- app/src/modules/SystemApi.ts | 26 +- app/src/modules/server/AdminController.tsx | 133 ++++++++--- app/src/modules/server/SystemController.ts | 1 + app/src/ui/Admin.tsx | 14 +- app/src/ui/client/BkndProvider.tsx | 4 +- app/src/ui/client/schema/actions.ts | 225 +++++------------- app/src/ui/client/schema/auth/use-auth.ts | 5 +- .../ui/client/schema/auth/use-bknd-auth.ts | 1 + app/src/ui/client/utils/AppQueryClient.ts | 3 +- app/src/ui/components/display/Alert.tsx | 26 +- app/src/ui/components/table/DataTable.tsx | 2 +- app/src/ui/layouts/AppShell/Header.tsx | 3 +- app/src/ui/modules/auth/LoginForm.tsx | 150 ++++-------- app/src/ui/modules/server/FlashMessage.tsx | 39 +++ app/src/ui/routes/auth/auth.login.tsx | 76 ++---- .../ui/routes/auth/auth.roles.edit.$role.tsx | 4 +- app/src/ui/routes/auth/auth.roles.tsx | 20 +- .../ui/routes/data/data.schema.$entity.tsx | 10 +- app/src/ui/routes/index.tsx | 2 +- app/src/ui/styles.css | 4 + biome.json | 3 + 30 files changed, 630 insertions(+), 483 deletions(-) create mode 100644 app/src/core/server/flash.ts create mode 100644 app/src/ui/modules/server/FlashMessage.tsx diff --git a/app/src/Api.ts b/app/src/Api.ts index b413819..30d58aa 100644 --- a/app/src/Api.ts +++ b/app/src/Api.ts @@ -4,13 +4,19 @@ import { decodeJwt } from "jose"; import { MediaApi } from "media/api/MediaApi"; import { SystemApi } from "modules/SystemApi"; +declare global { + interface Window { + __BKND__: { + user?: any; + }; + } +} + export type ApiOptions = { host: string; token?: string; - tokenStorage?: "localStorage"; - localStorage?: { - key?: string; - }; + storage?: "localStorage" | "manual"; + key?: string; }; export class Api { @@ -33,16 +39,25 @@ export class Api { this.buildApis(); } - private extractToken() { - if (this.options.tokenStorage === "localStorage") { - const key = this.options.localStorage?.key ?? "auth"; - const raw = localStorage.getItem(key); + get tokenStorage() { + return this.options.storage ?? "manual"; + } + get tokenKey() { + return this.options.key ?? "auth"; + } - if (raw) { - const { token } = JSON.parse(raw); + private extractToken() { + if (this.tokenStorage === "localStorage") { + const token = localStorage.getItem(this.tokenKey); + if (token) { this.token = token; this.user = decodeJwt(token) as any; } + } else { + if (typeof window !== "undefined" && "__BKND__" in window) { + this.user = window.__BKND__.user; + this.verified = true; + } } } @@ -50,11 +65,11 @@ export class Api { this.token = token; this.user = token ? (decodeJwt(token) as any) : undefined; - if (this.options.tokenStorage === "localStorage") { - const key = this.options.localStorage?.key ?? "auth"; + if (this.tokenStorage === "localStorage") { + const key = this.tokenKey; if (token) { - localStorage.setItem(key, JSON.stringify({ token })); + localStorage.setItem(key, token); } else { localStorage.removeItem(key); } @@ -69,8 +84,6 @@ export class Api { } getAuthState() { - if (!this.token) return; - return { token: this.token, user: this.user, diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index e0b53ce..7160cef 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -20,6 +20,7 @@ type AuthSchema = Static; export class AppAuth extends Module { private _authenticator?: Authenticator; cache: Record = {}; + _controller!: AuthController; override async onBeforeUpdate(from: AuthSchema, to: AuthSchema) { const defaultSecret = authConfigSchema.properties.jwt.properties.secret.default; @@ -68,9 +69,17 @@ export class AppAuth extends Module { this.registerEntities(); super.setBuilt(); - const controller = new AuthController(this); + this._controller = new AuthController(this); //this.ctx.server.use(controller.getMiddleware); - this.ctx.server.route(this.config.basepath, controller.getController()); + this.ctx.server.route(this.config.basepath, this._controller.getController()); + } + + get controller(): AuthController { + if (!this.isBuilt()) { + throw new Error("Can't access controller, AppAuth not built yet"); + } + + return this._controller; } getMiddleware() { diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts index a616bd0..ea07960 100644 --- a/app/src/auth/api/AuthController.ts +++ b/app/src/auth/api/AuthController.ts @@ -1,7 +1,6 @@ import type { AppAuth } from "auth"; import type { ClassController } from "core"; import { Hono, type MiddlewareHandler } from "hono"; -import * as SystemPermissions from "modules/permissions"; export class AuthController implements ClassController { constructor(private auth: AppAuth) {} @@ -11,19 +10,8 @@ export class AuthController implements ClassController { } getMiddleware: MiddlewareHandler = async (c, next) => { - let token: string | undefined; - if (c.req.raw.headers.has("Authorization")) { - const bearerHeader = String(c.req.header("Authorization")); - token = bearerHeader.replace("Bearer ", ""); - } - - if (token) { - // @todo: don't extract user from token, but from the database or cache - await this.auth.authenticator.verify(token); - this.auth.ctx.guard.setUserContext(this.auth.authenticator.getUser()); - } else { - this.auth.authenticator.__setUserNull(); - } + const user = await this.auth.authenticator.resolveAuthFromRequest(c); + this.auth.ctx.guard.setUserContext(user); await next(); }; @@ -31,7 +19,6 @@ export class AuthController implements ClassController { getController(): Hono { const hono = new Hono(); const strategies = this.auth.authenticator.getStrategies(); - //console.log("strategies", strategies); for (const [name, strategy] of Object.entries(strategies)) { //console.log("registering", name, "at", `/${name}`); @@ -46,6 +33,11 @@ export class AuthController implements ClassController { return c.json({ user: null }, 403); }); + hono.get("/logout", async (c) => { + await this.auth.authenticator.logout(c); + return c.json({ ok: true }); + }); + hono.get("/strategies", async (c) => { const { strategies, basepath } = this.auth.toJSON(false); return c.json({ strategies, basepath }); diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index f6114a3..358b080 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -1,5 +1,8 @@ +import { Exception } from "core"; +import { addFlashMessage } from "core/server/flash"; import { type Static, type TSchema, Type, parse, randomString, transformObject } from "core/utils"; -import type { Hono } from "hono"; +import type { Context, Hono } from "hono"; +import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { type JWTVerifyOptions, SignJWT, jwtVerify } from "jose"; type Input = any; // workaround @@ -82,7 +85,7 @@ export class Authenticator = Record< strategy: Strategy, identifier: string, profile: ProfileExchange - ) { + ): Promise { //console.log("resolve", { action, strategy: strategy.getName(), profile }); const user = await this.userResolver(action, strategy, identifier, profile); @@ -176,6 +179,84 @@ export class Authenticator = Record< return false; } + // @todo: CookieOptions not exported from hono + private get cookieOptions(): any { + return { + path: "/", + sameSite: "lax", + httpOnly: true + }; + } + + private async getAuthCookie(c: Context): Promise { + const secret = this.config.jwt.secret; + + const token = await getSignedCookie(c, secret, "auth"); + if (typeof token !== "string") { + await deleteCookie(c, "auth", this.cookieOptions); + return undefined; + } + + return token; + } + + private async setAuthCookie(c: Context, token: string) { + const secret = this.config.jwt.secret; + await setSignedCookie(c, "auth", token, secret, this.cookieOptions); + } + + async logout(c: Context) { + const cookie = await this.getAuthCookie(c); + if (cookie) { + await deleteCookie(c, "auth", this.cookieOptions); + await addFlashMessage(c, "Signed out", "info"); + } + } + + isJsonRequest(c: Context): boolean { + //return c.req.header("Content-Type") === "application/x-www-form-urlencoded"; + return c.req.header("Content-Type") === "application/json"; + } + + async respond(c: Context, data: AuthResponse | Error | any, redirect?: string) { + if (this.isJsonRequest(c)) { + return c.json(data); + } + + const referer = new URL(redirect ?? c.req.header("Referer") ?? "/"); + + if ("token" in data) { + await this.setAuthCookie(c, data.token); + return c.redirect("/"); + } + + let message = "An error occured"; + if (data instanceof Exception) { + message = data.message; + } + + await addFlashMessage(c, message, "error"); + return c.redirect(referer); + } + + // @todo: don't extract user from token, but from the database or cache + async resolveAuthFromRequest(c: Context): Promise { + let token: string | undefined; + if (c.req.raw.headers.has("Authorization")) { + const bearerHeader = String(c.req.header("Authorization")); + token = bearerHeader.replace("Bearer ", ""); + } else { + token = await this.getAuthCookie(c); + } + + if (token) { + await this.verify(token); + return this._user; + } + + return undefined; + } + toJSON(secrets?: boolean) { return { ...this.config, diff --git a/app/src/auth/authenticate/strategies/PasswordStrategy.ts b/app/src/auth/authenticate/strategies/PasswordStrategy.ts index 36af6ec..ef940d7 100644 --- a/app/src/auth/authenticate/strategies/PasswordStrategy.ts +++ b/app/src/auth/authenticate/strategies/PasswordStrategy.ts @@ -1,7 +1,7 @@ import type { Authenticator, Strategy } from "auth"; import { type Static, StringEnum, Type, parse } from "core/utils"; import { hash } from "core/utils"; -import { Hono } from "hono"; +import { type Context, Hono } from "hono"; type LoginSchema = { username: string; password: string } | { email: string; password: string }; type RegisterSchema = { email: string; password: string; [key: string]: any }; @@ -54,22 +54,34 @@ export class PasswordStrategy implements Strategy { getController(authenticator: Authenticator): Hono { const hono = new Hono(); + async function getBody(c: Context) { + if (authenticator.isJsonRequest(c)) { + return await c.req.json(); + } else { + return Object.fromEntries((await c.req.formData()).entries()); + } + } + return hono .post("/login", async (c) => { - const body = (await c.req.json()) ?? {}; + const body = await getBody(c); - const payload = await this.login(body); - const data = await authenticator.resolve("login", this, payload.password, payload); + try { + const payload = await this.login(body); + const data = await authenticator.resolve("login", this, payload.password, payload); - return c.json(data); + return await authenticator.respond(c, data); + } catch (e) { + return await authenticator.respond(c, e); + } }) .post("/register", async (c) => { - const body = (await c.req.json()) ?? {}; + const body = await getBody(c); const payload = await this.register(body); const data = await authenticator.resolve("register", this, payload.password, payload); - return c.json(data); + return await authenticator.respond(c, data); }); } diff --git a/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts b/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts index 0214059..6015ebd 100644 --- a/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts +++ b/app/src/auth/authenticate/strategies/oauth/OAuthStrategy.ts @@ -1,5 +1,5 @@ import type { AuthAction, Authenticator, Strategy } from "auth"; -import { Exception } from "core"; +import { Exception, isDebug } from "core"; import { type Static, StringEnum, type TSchema, Type, filterKeys, parse } from "core/utils"; import { type Context, Hono } from "hono"; import { getSignedCookie, setSignedCookie } from "hono/cookie"; @@ -173,7 +173,7 @@ export class OAuthStrategy implements Strategy { const config = await this.getConfig(); const { client, as, type } = config; //console.log("config", config); - //console.log("callbackParams", callbackParams, options); + console.log("callbackParams", callbackParams, options); const parameters = oauth.validateAuthResponse( as, client, // no client_secret required @@ -216,7 +216,7 @@ export class OAuthStrategy implements Strategy { expectedNonce ); if (oauth.isOAuth2Error(result)) { - //console.log("callback.error", result); + console.log("callback.error", result); // @todo: Handle OAuth 2.0 response body error throw new OAuthCallbackException(result, "processAuthorizationCodeOpenIDResponse"); } @@ -317,10 +317,15 @@ export class OAuthStrategy implements Strategy { const secret = "secret"; const cookie_name = "_challenge"; - const setState = async ( - c: Context, - config: { state: string; action: AuthAction; redirect?: string } - ): Promise => { + type TState = { + state: string; + action: AuthAction; + redirect?: string; + mode: "token" | "cookie"; + }; + + const setState = async (c: Context, config: TState): Promise => { + console.log("--- setting state", config); await setSignedCookie(c, cookie_name, JSON.stringify(config), secret, { secure: true, httpOnly: true, @@ -329,12 +334,18 @@ export class OAuthStrategy implements Strategy { }); }; - const getState = async ( - c: Context - ): Promise<{ state: string; action: AuthAction; redirect?: string }> => { - const state = await getSignedCookie(c, secret, cookie_name); + const getState = async (c: Context): Promise => { + if (c.req.header("X-State-Challenge")) { + return { + state: c.req.header("X-State-Challenge"), + action: c.req.header("X-State-Action"), + mode: "token" + } as any; + } + + const value = await getSignedCookie(c, secret, cookie_name); try { - return JSON.parse(state as string); + return JSON.parse(value as string); } catch (e) { throw new Error("Invalid state"); } @@ -345,22 +356,68 @@ export class OAuthStrategy implements Strategy { const params = new URLSearchParams(url.search); const state = await getState(c); - console.log("url", url); + console.log("state", state); + + // @todo: add config option to determine if state.action is allowed + const redirect_uri = + state.mode === "cookie" + ? url.origin + url.pathname + : url.origin + url.pathname.replace("/callback", "/token"); const profile = await this.callback(params, { - redirect_uri: url.origin + url.pathname, + redirect_uri, state: state.state }); - const { user, token } = await auth.resolve(state.action, this, profile.sub, profile); - console.log("******** RESOLVED ********", { user, token }); + try { + const data = await auth.resolve(state.action, this, profile.sub, profile); + console.log("******** RESOLVED ********", data); - if (state.redirect) { - console.log("redirect to", state.redirect + "?token=" + token); - return c.redirect(state.redirect + "?token=" + token); + if (state.mode === "cookie") { + return await auth.respond(c, data, state.redirect); + } + + return c.json(data); + } catch (e) { + if (state.mode === "cookie") { + return await auth.respond(c, e, state.redirect); + } + + throw e; + } + }); + + hono.get("/token", async (c) => { + const url = new URL(c.req.url); + const params = new URLSearchParams(url.search); + + return c.json({ + code: params.get("code") ?? null + }); + }); + + hono.post("/:action", async (c) => { + const action = c.req.param("action") as AuthAction; + if (!["login", "register"].includes(action)) { + return c.notFound(); } - return c.json({ user, token }); + const url = new URL(c.req.url); + const path = url.pathname.replace(`/${action}`, ""); + const redirect_uri = url.origin + path + "/callback"; + const referer = new URL(c.req.header("Referer") ?? "/"); + + const state = oauth.generateRandomCodeVerifier(); + const response = await this.request({ + redirect_uri, + state + }); + //console.log("_state", state); + + await setState(c, { state, action, redirect: referer.toString(), mode: "cookie" }); + console.log("--redirecting to", response.url); + + return c.redirect(response.url); }); hono.get("/:action", async (c) => { @@ -371,31 +428,29 @@ export class OAuthStrategy implements Strategy { const url = new URL(c.req.url); const path = url.pathname.replace(`/${action}`, ""); - const redirect_uri = url.origin + path + "/callback"; - const q_redirect = (c.req.query("redirect") as string) ?? undefined; + const redirect_uri = url.origin + path + "/token"; - const state = await oauth.generateRandomCodeVerifier(); + const state = oauth.generateRandomCodeVerifier(); const response = await this.request({ redirect_uri, state }); - //console.log("_state", state); - await setState(c, { state, action, redirect: q_redirect }); - - if (c.req.header("Accept") === "application/json") { + if (isDebug()) { return c.json({ url: response.url, redirect_uri, challenge: state, + action, params: response.params }); } - //return c.text(response.url); - console.log("--redirecting to", response.url); - - return c.redirect(response.url); + return c.json({ + url: response.url, + challenge: state, + action + }); }); return hono; diff --git a/app/src/auth/authorize/Guard.ts b/app/src/auth/authorize/Guard.ts index f40348d..880d134 100644 --- a/app/src/auth/authorize/Guard.ts +++ b/app/src/auth/authorize/Guard.ts @@ -1,5 +1,5 @@ import { Exception, Permission } from "core"; -import { type Static, Type, objectTransform } from "core/utils"; +import { objectTransform } from "core/utils"; import { Role } from "./Role"; export type GuardUserContext = { diff --git a/app/src/core/server/flash.ts b/app/src/core/server/flash.ts new file mode 100644 index 0000000..c64753b --- /dev/null +++ b/app/src/core/server/flash.ts @@ -0,0 +1,40 @@ +import type { Context } from "hono"; +import { setCookie } from "hono/cookie"; + +const flash_key = "__bknd_flash"; +export type FlashMessageType = "error" | "warning" | "success" | "info"; + +export async function addFlashMessage( + c: Context, + message: string, + type: FlashMessageType = "info" +) { + setCookie(c, flash_key, JSON.stringify({ type, message }), { + path: "/" + }); +} + +function getCookieValue(name) { + const cookies = document.cookie.split("; "); + for (const cookie of cookies) { + const [key, value] = cookie.split("="); + if (key === name) { + try { + return decodeURIComponent(value as any); + } catch (e) { + return null; + } + } + } + return null; // Return null if the cookie is not found +} + +export function getFlashMessage( + clear = true +): { type: FlashMessageType; message: string } | undefined { + const flash = getCookieValue(flash_key); + if (flash && clear) { + document.cookie = `${flash_key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + } + return flash ? JSON.parse(flash) : undefined; +} diff --git a/app/src/modules/ModuleApi.ts b/app/src/modules/ModuleApi.ts index 4c6aa8d..3454a8d 100644 --- a/app/src/modules/ModuleApi.ts +++ b/app/src/modules/ModuleApi.ts @@ -64,7 +64,7 @@ export abstract class ModuleApi { } let body: any = _init?.body; - if (_init && "body" in _init && ["POST", "PATCH"].includes(method)) { + if (_init && "body" in _init && ["POST", "PATCH", "PUT"].includes(method)) { const requestContentType = (headers.get("Content-Type") as string) ?? undefined; if (!requestContentType || requestContentType.startsWith("application/json")) { body = JSON.stringify(_init.body); @@ -137,6 +137,18 @@ export abstract class ModuleApi { }); } + protected async put( + _input: string | (string | number | PrimaryFieldType)[], + body?: any, + _init?: RequestInit + ) { + return this.request(_input, undefined, { + ..._init, + body, + method: "PUT" + }); + } + protected async delete( _input: string | (string | number | PrimaryFieldType)[], _init?: RequestInit diff --git a/app/src/modules/SystemApi.ts b/app/src/modules/SystemApi.ts index 7dd056c..f1243da 100644 --- a/app/src/modules/SystemApi.ts +++ b/app/src/modules/SystemApi.ts @@ -1,5 +1,5 @@ import { ModuleApi } from "./ModuleApi"; -import type { ModuleConfigs, ModuleSchemas } from "./ModuleManager"; +import type { ModuleConfigs, ModuleKey, ModuleSchemas } from "./ModuleManager"; export type ApiSchemaResponse = { version: number; @@ -21,4 +21,28 @@ export class SystemApi extends ModuleApi { secrets: options?.secrets ? 1 : 0 }); } + + async setConfig( + module: Module, + value: ModuleConfigs[Module], + force?: boolean + ) { + return await this.post(["config", "set", module, `?force=${force ? 1 : 0}`], value); + } + + async addConfig(module: Module, path: string, value: any) { + return await this.post(["config", "add", module, path], value); + } + + async patchConfig(module: Module, path: string, value: any) { + return await this.patch(["config", "patch", module, path], value); + } + + async overwriteConfig(module: Module, path: string, value: any) { + return await this.put(["config", "overwrite", module, path], value); + } + + async removeConfig(module: Module, path: string) { + return await this.delete(["config", "remove", module, path]); + } } diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index e600d9d..fd65c68 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -2,8 +2,9 @@ import type { App } from "App"; import { type ClassController, isDebug } from "core"; +import { addFlashMessage } from "core/server/flash"; import { Hono } from "hono"; -import { html, raw } from "hono/html"; +import { html } from "hono/html"; import { Fragment } from "hono/jsx"; import * as SystemPermissions from "modules/permissions"; import type { Manifest } from "vite"; @@ -21,6 +22,12 @@ export type AdminControllerOptions = { viteManifest?: Manifest; }; +const authRoutes = { + root: "/", + login: "/auth/login", + logout: "/auth/logout" +}; + export class AdminController implements ClassController { constructor( private readonly app: App, @@ -31,43 +38,97 @@ export class AdminController implements ClassController { return this.app.modules.ctx(); } - getController(): Hono { - const hono = new Hono(); + getController(): Hono { + const auth = this.app.module.auth; const configs = this.app.modules.configs(); + const auth_enabled = configs.auth.enabled; const basepath = (String(configs.server.admin.basepath) + "/").replace(/\/+$/, "/"); + const hono = new Hono<{ + Variables: { + html: string; + }; + }>().basePath(basepath); - this.ctx.server.get(basepath + "*", async (c) => { - if (this.options.html) { - return c.html(this.options.html); + hono.use("*", async (c, next) => { + const obj = { user: auth.authenticator.getUser() }; + 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); + await next(); + }); + + hono.get(authRoutes.login, async (c) => { + if ( + this.app.module.auth.authenticator.isUserLoggedIn() && + this.ctx.guard.granted(SystemPermissions.admin) + ) { + return c.redirect(authRoutes.root); } - // @todo: implement guard redirect once cookie sessions arrive + const html = c.get("html"); + return c.html(html); + }); - const isProd = !isDebug(); - let script: string | undefined; - let css: string[] = []; + hono.get(authRoutes.logout, async (c) => { + await auth.authenticator.logout(c); + return c.redirect(authRoutes.login); + }); - if (isProd) { - const manifest: Manifest = this.options.viteManifest - ? this.options.viteManifest - : isProd - ? // @ts-ignore cases issues when building types - await import("bknd/dist/manifest.json", { assert: { type: "json" } }).then( - (m) => m.default - ) - : {}; - //console.log("manifest", manifest, manifest["index.html"]); - const entry = Object.values(manifest).find((f: any) => f.isEntry === true); - if (!entry) { - // do something smart - return; - } - - script = "/" + entry.file; - css = entry.css?.map((c: string) => "/" + c) ?? []; + hono.get("*", async (c) => { + console.log("admin", c.req.url); + if (!this.ctx.guard.granted(SystemPermissions.admin)) { + await addFlashMessage(c, "You are not authorized to access the Admin UI", "error"); + return c.redirect(authRoutes.login); } - return c.html( + const html = c.get("html"); + return c.html(html); + }); + + return hono; + } + + private async getHtml(obj: any = {}) { + if (this.options.html) { + // @todo: add __BKND__ global + return this.options.html as string; + } + + const configs = this.app.modules.configs(); + + // @todo: implement guard redirect once cookie sessions arrive + + const isProd = !isDebug(); + let script: string | undefined; + let css: string[] = []; + + if (isProd) { + const manifest: Manifest = this.options.viteManifest + ? this.options.viteManifest + : isProd + ? // @ts-ignore cases issues when building types + await import("bknd/dist/manifest.json", { assert: { type: "json" } }).then( + (m) => m.default + ) + : {}; + //console.log("manifest", manifest, manifest["index.html"]); + const entry = Object.values(manifest).find((f: any) => f.isEntry === true); + if (!entry) { + // do something smart + return; + } + + script = "/" + entry.file; + css = entry.css?.map((c: string) => "/" + c) ?? []; + } + + return ( + + {html``} @@ -85,20 +146,22 @@ export class AdminController implements ClassController { ) : ( - {/* biome-ignore lint/security/noDangerouslySetInnerHtml: I know what I do here :) */}