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>
<body>
<div id="app"></div>
<!-- BKND_CONTEXT -->
<script type="module" src="/src/ui/main.tsx"></script>
</body>
</html>

View File

@@ -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;
}

View File

@@ -55,12 +55,14 @@ export interface UserPool<Fields = "id" | "email" | "username"> {
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<Strategies extends Record<string, Strategy> = 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);

View File

@@ -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
*

View File

@@ -16,18 +16,13 @@ window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
`;
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
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 '<!-- BKND_CONTEXT -->' to inject BKND context"
);
return this.options.html as string;
}
@@ -168,7 +178,7 @@ export class AdminController implements ClassController {
<div id="app" />
<script
dangerouslySetInnerHTML={{
__html: `window.__BKND__ = JSON.parse('${JSON.stringify(obj)}');`
__html: bknd_context
}}
/>
{!isProd && <script type="module" src="/src/ui/main.tsx" />}

View File

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

Binary file not shown.