verified cloudflare workers + added configs where to navigate after login/logout

This commit is contained in:
dswbx
2024-11-26 13:26:11 +01:00
parent 9d896a6ab1
commit d36c4b07e0
7 changed files with 79 additions and 27 deletions

View File

@@ -7,6 +7,7 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<!-- BKND_CONTEXT -->
<script type="module" src="/src/ui/main.tsx"></script> <script type="module" src="/src/ui/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -4,19 +4,15 @@ import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers"; import { serveStatic } from "hono/cloudflare-workers";
import type { BkndConfig, CfBkndModeCache } from "../index"; import type { BkndConfig, CfBkndModeCache } from "../index";
// @ts-ignore
import _html from "../../static/index.html";
type Context = { type Context = {
request: Request; request: Request;
env: any; env: any;
ctx: ExecutionContext; ctx: ExecutionContext;
manifest: any; manifest: any;
html: string; html?: string;
}; };
export function serve(_config: BkndConfig, manifest?: string, overrideHtml?: string) { export function serve(_config: BkndConfig, manifest?: string, html?: string) {
const html = overrideHtml ?? _html;
return { return {
async fetch(request: Request, env: any, ctx: ExecutionContext) { async fetch(request: Request, env: any, ctx: ExecutionContext) {
const url = new URL(request.url); const url = new URL(request.url);
@@ -182,7 +178,7 @@ export class DurableBkndApp extends DurableObject {
request: Request, request: Request,
options: { options: {
config: CreateAppConfig; config: CreateAppConfig;
html: string; html?: string;
keepAliveSeconds?: number; keepAliveSeconds?: number;
setAdminHtml?: boolean; setAdminHtml?: boolean;
} }

View File

@@ -55,12 +55,14 @@ export interface UserPool<Fields = "id" | "email" | "username"> {
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
export const cookieConfig = Type.Partial( export const cookieConfig = Type.Partial(
Type.Object({ Type.Object({
renew: Type.Boolean({ default: true }),
path: Type.String({ default: "/" }), path: Type.String({ default: "/" }),
sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }), sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }),
secure: Type.Boolean({ default: true }), secure: Type.Boolean({ default: true }),
httpOnly: 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 } { default: {}, additionalProperties: false }
); );
@@ -257,12 +259,11 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
return c.json(data); return c.json(data);
} }
const successPath = "/"; const successPath = this.config.cookie.pathSuccess ?? "/";
const successUrl = new URL(c.req.url).origin + successPath.replace(/\/+$/, "/"); const successUrl = new URL(c.req.url).origin + successPath.replace(/\/+$/, "/");
const referer = new URL(redirect ?? c.req.header("Referer") ?? successUrl); const referer = new URL(redirect ?? c.req.header("Referer") ?? successUrl);
if ("token" in data) { if ("token" in data) {
// @todo: add config
await this.setAuthCookie(c, data.token); await this.setAuthCookie(c, data.token);
// can't navigate to "/" doesn't work on nextjs // can't navigate to "/" doesn't work on nextjs
return c.redirect(successUrl); return c.redirect(successUrl);

View File

@@ -15,10 +15,6 @@ export function ucFirstAll(str: string, split: string = " "): string {
.join(split); .join(split);
} }
export function ucFirstAllSnakeToPascalWithSpaces(str: string, split: string = " "): string {
return ucFirstAll(snakeToPascalWithSpaces(str), split);
}
export function randomString(length: number, includeSpecial = false): string { export function randomString(length: number, includeSpecial = false): string {
const base = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const base = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const special = "!@#$%^&*()_+{}:\"<>?|[];',./`~"; const special = "!@#$%^&*()_+{}:\"<>?|[];',./`~";
@@ -49,6 +45,54 @@ export function pascalToKebab(pascalStr: string): string {
return pascalStr.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); 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 * Replace simple mustache like {placeholders} in a string
* *

View File

@@ -16,18 +16,13 @@ window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true window.__vite_plugin_react_preamble_installed__ = true
`; `;
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
export type AdminControllerOptions = { export type AdminControllerOptions = {
html?: string; html?: string;
viteManifest?: Manifest; viteManifest?: Manifest;
}; };
const authRoutes = {
root: "/",
login: "/auth/login",
logout: "/auth/logout"
};
export class AdminController implements ClassController { export class AdminController implements ClassController {
constructor( constructor(
private readonly app: App, private readonly app: App,
@@ -52,6 +47,13 @@ export class AdminController implements ClassController {
html: string; html: string;
}; };
}>().basePath(this.withBasePath()); }>().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) => { hono.use("*", async (c, next) => {
const obj = { const obj = {
@@ -77,7 +79,7 @@ export class AdminController implements ClassController {
this.app.module.auth.authenticator?.isUserLoggedIn() && this.app.module.auth.authenticator?.isUserLoggedIn() &&
this.ctx.guard.granted(SystemPermissions.accessAdmin) this.ctx.guard.granted(SystemPermissions.accessAdmin)
) { ) {
return c.redirect(authRoutes.root); return c.redirect(authRoutes.success);
} }
const html = c.get("html"); const html = c.get("html");
@@ -86,7 +88,7 @@ export class AdminController implements ClassController {
hono.get(authRoutes.logout, async (c) => { hono.get(authRoutes.logout, async (c) => {
await auth.authenticator?.logout(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 = {}) { private async getHtml(obj: any = {}) {
const bknd_context = `window.__BKND__ = JSON.parse('${JSON.stringify(obj)}');`;
if (this.options.html) { 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 '<!-- BKND_CONTEXT -->' to inject BKND context"
);
return this.options.html as string; return this.options.html as string;
} }
@@ -168,7 +178,7 @@ export class AdminController implements ClassController {
<div id="app" /> <div id="app" />
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `window.__BKND__ = JSON.parse('${JSON.stringify(obj)}');` __html: bknd_context
}} }}
/> />
{!isProd && <script type="module" src="/src/ui/main.tsx" />} {!isProd && <script type="module" src="/src/ui/main.tsx" />}

View File

@@ -6,7 +6,7 @@ import {
getTemplate, getTemplate,
getUiOptions getUiOptions
} from "@rjsf/utils"; } from "@rjsf/utils";
import { ucFirstAll, ucFirstAllSnakeToPascalWithSpaces } from "core/utils"; import { identifierToHumanReadable } from "core/utils";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
const REQUIRED_FIELD_SYMBOL = "*"; const REQUIRED_FIELD_SYMBOL = "*";
@@ -31,7 +31,7 @@ export function Label(props: LabelProps) {
} }
return ( return (
<label className="control-label" htmlFor={id}> <label className="control-label" htmlFor={id}>
{ucFirstAllSnakeToPascalWithSpaces(label)} {identifierToHumanReadable(label)}
{required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>} {required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>}
</label> </label>
); );

Binary file not shown.