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>
|
</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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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" />}
|
||||||
|
|||||||
@@ -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.
Reference in New Issue
Block a user