diff --git a/app/index.html b/app/index.html index 2efd6e2..47f4b01 100644 --- a/app/index.html +++ b/app/index.html @@ -7,6 +7,7 @@
+ diff --git a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts index 9dc071c..5224c10 100644 --- a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts +++ b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts @@ -4,19 +4,15 @@ import { Hono } from "hono"; import { serveStatic } from "hono/cloudflare-workers"; import type { BkndConfig, CfBkndModeCache } from "../index"; -// @ts-ignore -import _html from "../../static/index.html"; - type Context = { request: Request; env: any; ctx: ExecutionContext; manifest: any; - html: string; + html?: string; }; -export function serve(_config: BkndConfig, manifest?: string, overrideHtml?: string) { - const html = overrideHtml ?? _html; +export function serve(_config: BkndConfig, manifest?: string, html?: string) { return { async fetch(request: Request, env: any, ctx: ExecutionContext) { const url = new URL(request.url); @@ -182,7 +178,7 @@ export class DurableBkndApp extends DurableObject { request: Request, options: { config: CreateAppConfig; - html: string; + html?: string; keepAliveSeconds?: number; setAdminHtml?: boolean; } diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 32c7b42..082cd1c 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -55,12 +55,14 @@ export interface UserPool { const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds export const cookieConfig = Type.Partial( Type.Object({ - renew: Type.Boolean({ default: true }), path: Type.String({ default: "/" }), sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }), secure: Type.Boolean({ default: true }), httpOnly: Type.Boolean({ default: true }), - expires: Type.Number({ default: defaultCookieExpires }) // seconds + expires: Type.Number({ default: defaultCookieExpires }), // seconds + renew: Type.Boolean({ default: true }), + pathSuccess: Type.String({ default: "/" }), + pathLoggedOut: Type.String({ default: "/" }) }), { default: {}, additionalProperties: false } ); @@ -257,12 +259,11 @@ export class Authenticator = Record< return c.json(data); } - const successPath = "/"; + const successPath = this.config.cookie.pathSuccess ?? "/"; const successUrl = new URL(c.req.url).origin + successPath.replace(/\/+$/, "/"); const referer = new URL(redirect ?? c.req.header("Referer") ?? successUrl); if ("token" in data) { - // @todo: add config await this.setAuthCookie(c, data.token); // can't navigate to "/" – doesn't work on nextjs return c.redirect(successUrl); diff --git a/app/src/core/utils/strings.ts b/app/src/core/utils/strings.ts index adc68b7..a5a7c72 100644 --- a/app/src/core/utils/strings.ts +++ b/app/src/core/utils/strings.ts @@ -15,10 +15,6 @@ export function ucFirstAll(str: string, split: string = " "): string { .join(split); } -export function ucFirstAllSnakeToPascalWithSpaces(str: string, split: string = " "): string { - return ucFirstAll(snakeToPascalWithSpaces(str), split); -} - export function randomString(length: number, includeSpecial = false): string { const base = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const special = "!@#$%^&*()_+{}:\"<>?|[];',./`~"; @@ -49,6 +45,54 @@ export function pascalToKebab(pascalStr: string): string { return pascalStr.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); } +type StringCaseType = + | "snake_case" + | "PascalCase" + | "camelCase" + | "kebab-case" + | "SCREAMING_SNAKE_CASE" + | "unknown"; +export function detectCase(input: string): StringCaseType { + if (/^[a-z]+(_[a-z]+)*$/.test(input)) { + return "snake_case"; + } else if (/^[A-Z][a-zA-Z]*$/.test(input)) { + return "PascalCase"; + } else if (/^[a-z][a-zA-Z]*$/.test(input)) { + return "camelCase"; + } else if (/^[a-z]+(-[a-z]+)*$/.test(input)) { + return "kebab-case"; + } else if (/^[A-Z]+(_[A-Z]+)*$/.test(input)) { + return "SCREAMING_SNAKE_CASE"; + } else { + return "unknown"; + } +} +export function identifierToHumanReadable(str: string) { + const _case = detectCase(str); + switch (_case) { + case "snake_case": + return snakeToPascalWithSpaces(str); + case "PascalCase": + return kebabToPascalWithSpaces(pascalToKebab(str)); + case "camelCase": + return ucFirst(kebabToPascalWithSpaces(pascalToKebab(str))); + case "kebab-case": + return kebabToPascalWithSpaces(str); + case "SCREAMING_SNAKE_CASE": + return snakeToPascalWithSpaces(str.toLowerCase()); + case "unknown": + return str; + } +} + +export function kebabToPascalWithSpaces(str: string): string { + return str.split("-").map(ucFirst).join(" "); +} + +export function ucFirstAllSnakeToPascalWithSpaces(str: string, split: string = " "): string { + return ucFirstAll(snakeToPascalWithSpaces(str), split); +} + /** * Replace simple mustache like {placeholders} in a string * diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index 96af5c1..cd252a5 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -16,18 +16,13 @@ window.$RefreshReg$ = () => {} window.$RefreshSig$ = () => (type) => type window.__vite_plugin_react_preamble_installed__ = true `; +const htmlBkndContextReplace = ""; export type AdminControllerOptions = { html?: string; viteManifest?: Manifest; }; -const authRoutes = { - root: "/", - login: "/auth/login", - logout: "/auth/logout" -}; - export class AdminController implements ClassController { constructor( private readonly app: App, @@ -52,6 +47,13 @@ export class AdminController implements ClassController { 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 = { @@ -77,7 +79,7 @@ export class AdminController implements ClassController { this.app.module.auth.authenticator?.isUserLoggedIn() && this.ctx.guard.granted(SystemPermissions.accessAdmin) ) { - return c.redirect(authRoutes.root); + return c.redirect(authRoutes.success); } const html = c.get("html"); @@ -86,7 +88,7 @@ export class AdminController implements ClassController { hono.get(authRoutes.logout, async (c) => { await auth.authenticator?.logout(c); - return c.redirect(authRoutes.login); + return c.redirect(authRoutes.loggedOut); }); } @@ -104,8 +106,16 @@ export class AdminController implements ClassController { } private async getHtml(obj: any = {}) { + const bknd_context = `window.__BKND__ = JSON.parse('${JSON.stringify(obj)}');`; + if (this.options.html) { - // @todo: add __BKND__ global + if (this.options.html.includes(htmlBkndContextReplace)) { + return this.options.html.replace(htmlBkndContextReplace, bknd_context); + } + + console.warn( + "Custom HTML needs to include '' to inject BKND context" + ); return this.options.html as string; } @@ -168,7 +178,7 @@ export class AdminController implements ClassController {