mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
verified cloudflare workers + added configs where to navigate after login/logout
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- BKND_CONTEXT -->
|
||||
<script type="module" src="/src/ui/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user