mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
feat: add local auth support if api storage provided
This commit is contained in:
@@ -188,6 +188,7 @@ async function buildUiElements() {
|
||||
"ui/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
"wouter",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"hono": "4.10.4",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"jsonv-ts": "0.9.3",
|
||||
"jsonv-ts": "^0.10.1",
|
||||
"kysely": "0.28.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
|
||||
@@ -61,7 +61,7 @@ export class Api {
|
||||
private token?: string;
|
||||
private user?: TApiUser;
|
||||
private verified = false;
|
||||
private token_transport: "header" | "cookie" | "none" = "header";
|
||||
public token_transport: "header" | "cookie" | "none" = "header";
|
||||
|
||||
public system!: SystemApi;
|
||||
public data!: DataApi;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { checksum } from "bknd/utils";
|
||||
import { App, registries, sqlocal, type BkndConfig } from "bknd";
|
||||
import { Route, Router, Switch } from "wouter";
|
||||
import { type Api, ClientProvider } from "bknd/client";
|
||||
import { ClientProvider } from "bknd/client";
|
||||
import { SQLocalKysely } from "sqlocal/kysely";
|
||||
import type { ClientConfig, DatabasePath } from "sqlocal";
|
||||
import { OpfsStorageAdapter } from "bknd/adapter/browser";
|
||||
@@ -44,6 +44,7 @@ export type BrowserBkndConfig<Args = ImportMetaEnv> = Omit<
|
||||
|
||||
export type BkndBrowserAppProps = {
|
||||
children: ReactNode;
|
||||
header?: ReactNode;
|
||||
loading?: ReactNode;
|
||||
notFound?: ReactNode;
|
||||
} & BrowserBkndConfig;
|
||||
@@ -56,19 +57,18 @@ const BkndBrowserAppContext = createContext<{
|
||||
export function BkndBrowserApp({
|
||||
children,
|
||||
adminConfig,
|
||||
header,
|
||||
loading,
|
||||
notFound,
|
||||
...config
|
||||
}: BkndBrowserAppProps) {
|
||||
const [app, setApp] = useState<App | undefined>(undefined);
|
||||
const [api, setApi] = useState<Api | undefined>(undefined);
|
||||
const [hash, setHash] = useState<string>("");
|
||||
const adminRoutePath = (adminConfig?.basepath ?? "") + "/*?";
|
||||
|
||||
async function onBuilt(app: App) {
|
||||
safeViewTransition(async () => {
|
||||
setApp(app);
|
||||
setApi(app.getApi());
|
||||
setHash(await checksum(app.toJSON()));
|
||||
});
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export function BkndBrowserApp({
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
if (!app || !api) {
|
||||
if (!app) {
|
||||
return (
|
||||
loading ?? (
|
||||
<Center>
|
||||
@@ -91,7 +91,8 @@ export function BkndBrowserApp({
|
||||
|
||||
return (
|
||||
<BkndBrowserAppContext.Provider value={{ app, hash }}>
|
||||
<ClientProvider api={api}>
|
||||
<ClientProvider storage={window.localStorage} fetcher={app.server.request}>
|
||||
{header}
|
||||
<Router key={hash}>
|
||||
<Switch>
|
||||
{children}
|
||||
|
||||
@@ -32,6 +32,7 @@ export function getFlashMessage(
|
||||
): { type: FlashMessageType; message: string } | undefined {
|
||||
const flash = getCookieValue(flash_key);
|
||||
if (flash && clear) {
|
||||
// biome-ignore lint/suspicious/noDocumentCookie: .
|
||||
document.cookie = `${flash_key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
}
|
||||
return flash ? JSON.parse(flash) : undefined;
|
||||
|
||||
@@ -87,7 +87,7 @@ export type ModuleManagerOptions = {
|
||||
verbosity?: Verbosity;
|
||||
};
|
||||
|
||||
const debug_modules = env("modules_debug");
|
||||
const debug_modules = env("modules_debug", false);
|
||||
|
||||
abstract class ModuleManagerEvent<A = {}> extends Event<{ ctx: ModuleBuildContext } & A> {}
|
||||
export class ModuleManagerConfigUpdateEvent<
|
||||
|
||||
@@ -67,13 +67,14 @@ export const ClientProvider = ({
|
||||
|
||||
export const useApi = (host?: ApiOptions["host"]): Api => {
|
||||
const context = useContext(ClientContext);
|
||||
if (!context) {
|
||||
throw new Error("useApi must be used within a ClientProvider");
|
||||
}
|
||||
|
||||
if (!context?.api || (host && host.length > 0 && host !== context.baseUrl)) {
|
||||
console.info("creating new api", { host });
|
||||
return new Api({ host: host ?? "" });
|
||||
}
|
||||
if (!context) {
|
||||
throw new Error("useApi must be used within a ClientProvider");
|
||||
}
|
||||
|
||||
return context.api;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ type UseAuth = {
|
||||
logout: () => Promise<void>;
|
||||
verify: () => Promise<void>;
|
||||
setToken: (token: string) => void;
|
||||
local: boolean;
|
||||
};
|
||||
|
||||
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||
@@ -60,5 +61,6 @@ export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||
logout,
|
||||
setToken,
|
||||
verify,
|
||||
local: !!api.options.storage,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type React from "react";
|
||||
import { Children } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { Children, forwardRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Tooltip } from "@mantine/core";
|
||||
import clsx from "clsx";
|
||||
import { getBrowser } from "core/utils";
|
||||
import { getBrowser } from "bknd/utils";
|
||||
import type { Field } from "data/fields";
|
||||
import { Switch as RadixSwitch } from "radix-ui";
|
||||
import {
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
setPath,
|
||||
} from "./utils";
|
||||
|
||||
export type NativeFormProps = {
|
||||
export type NativeFormProps = Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit"> & {
|
||||
hiddenSubmit?: boolean;
|
||||
validateOn?: "change" | "submit";
|
||||
errorFieldSelector?: <K extends keyof HTMLElementTagNameMap>(name: string) => any | null;
|
||||
errorFieldSelector?: (selector: string) => any | null;
|
||||
reportValidity?: boolean;
|
||||
onSubmit?: (data: any, ctx: { event: FormEvent<HTMLFormElement> }) => Promise<void> | void;
|
||||
onSubmitInvalid?: (
|
||||
@@ -33,7 +33,7 @@ export type NativeFormProps = {
|
||||
ctx: { event: ChangeEvent<HTMLFormElement>; key: string; value: any; errors: InputError[] },
|
||||
) => Promise<void> | void;
|
||||
clean?: CleanOptions | true;
|
||||
} & Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit">;
|
||||
};
|
||||
|
||||
export type InputError = {
|
||||
name: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useInsertionEffect, useRef } from "react";
|
||||
import { type LinkProps, Link as WouterLink, useRoute, useRouter } from "wouter";
|
||||
import { type LinkProps, Link as WouterLink, useRouter } from "wouter";
|
||||
import { useEvent } from "../../hooks/use-event";
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema";
|
||||
import clsx from "clsx";
|
||||
import { NativeForm } from "ui/components/form/native-form/NativeForm";
|
||||
import { transform } from "lodash-es";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { transformObject } from "bknd/utils";
|
||||
import { useEffect, useState, type ComponentPropsWithoutRef, type FormEvent } from "react";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { Group, Input, Password, Label } from "ui/components/form/Formy/components";
|
||||
import { SocialLink } from "./SocialLink";
|
||||
import { useAuth } from "bknd/client";
|
||||
import { Alert } from "ui/components/display/Alert";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
|
||||
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "action"> & {
|
||||
className?: string;
|
||||
formData?: any;
|
||||
action: "login" | "register";
|
||||
@@ -23,25 +26,47 @@ export function AuthForm({
|
||||
action,
|
||||
auth,
|
||||
buttonLabel = action === "login" ? "Sign in" : "Sign up",
|
||||
onSubmit: _onSubmit,
|
||||
...props
|
||||
}: LoginFormProps) {
|
||||
const $auth = useAuth();
|
||||
const basepath = auth?.basepath ?? "/api/auth";
|
||||
const [error, setError] = useState<string>();
|
||||
const [, navigate] = useLocation();
|
||||
const password = {
|
||||
action: `${basepath}/password/${action}`,
|
||||
strategy: auth?.strategies?.password ?? ({ type: "password" } as const),
|
||||
};
|
||||
|
||||
const oauth = transform(
|
||||
auth?.strategies ?? {},
|
||||
(result, value, key) => {
|
||||
if (value.type !== "password") {
|
||||
result[key] = value.config;
|
||||
}
|
||||
},
|
||||
{},
|
||||
) as Record<string, AppAuthOAuthStrategy>;
|
||||
const oauth = transformObject(auth?.strategies ?? {}, (value) => {
|
||||
return value.type !== "password" ? value.config : undefined;
|
||||
}) as Record<string, AppAuthOAuthStrategy>;
|
||||
const has_oauth = Object.keys(oauth).length > 0;
|
||||
|
||||
async function onSubmit(data: any, ctx: { event: FormEvent<HTMLFormElement> }) {
|
||||
if ($auth?.local) {
|
||||
ctx.event.preventDefault();
|
||||
|
||||
const res = await $auth.login(data);
|
||||
if ("token" in res) {
|
||||
navigate("/");
|
||||
} else {
|
||||
setError((res as any).error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await _onSubmit?.(ctx.event);
|
||||
// submit form
|
||||
ctx.event.currentTarget.submit();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if ($auth.user) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [$auth.user]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{has_oauth && (
|
||||
@@ -63,10 +88,12 @@ export function AuthForm({
|
||||
<NativeForm
|
||||
method={method}
|
||||
action={password.action}
|
||||
onSubmit={onSubmit}
|
||||
{...(props as any)}
|
||||
validateOn="change"
|
||||
className={clsx("flex flex-col gap-3 w-full", className)}
|
||||
>
|
||||
{error && <Alert.Exception message={error} className="justify-center" />}
|
||||
<Group>
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
<Input type="email" name="email" required />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
|
||||
import { ucFirstAllSnakeToPascalWithSpaces } from "bknd/utils";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import type { IconType } from "ui/components/buttons/IconButton";
|
||||
|
||||
@@ -154,8 +154,10 @@ function UserMenu() {
|
||||
|
||||
async function handleLogout() {
|
||||
await auth.logout();
|
||||
// @todo: grab from somewhere constant
|
||||
navigate(logout_route, { reload: true });
|
||||
|
||||
if (!auth.local) {
|
||||
navigate(logout_route, { reload: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { McpClient, type McpClientConfig } from "jsonv-ts/mcp";
|
||||
import { useApi } from "bknd/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
const clients = new Map<string, McpClient>();
|
||||
@@ -12,5 +13,14 @@ export function getClient(opts: McpClientConfig) {
|
||||
|
||||
export function useMcpClient() {
|
||||
const { config } = useBknd();
|
||||
return getClient({ url: window.location.origin + config.server.mcp.path });
|
||||
const api = useApi();
|
||||
const token = api.getAuthState().token;
|
||||
const headers =
|
||||
api.token_transport === "header" && token ? { Authorization: `Bearer ${token}` } : undefined;
|
||||
|
||||
return getClient({
|
||||
url: window.location.origin + config.server.mcp.path,
|
||||
fetch: api.fetcher,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
export default function ToolsMcp() {
|
||||
useBrowserTitle(["MCP UI"]);
|
||||
|
||||
const { config, options } = useBknd();
|
||||
const { config } = useBknd();
|
||||
const feature = useMcpStore((state) => state.feature);
|
||||
const setFeature = useMcpStore((state) => state.setFeature);
|
||||
const content = useMcpStore((state) => state.content);
|
||||
|
||||
Reference in New Issue
Block a user