mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
added auth strategies form + add ability to disable strategies
This commit is contained in:
@@ -44,7 +44,7 @@ export function AuthRoot({ children }) {
|
||||
>
|
||||
Roles & Permissions
|
||||
</AppShell.SidebarLink>
|
||||
<AppShell.SidebarLink as={Link} href={routes.auth.strategies()} disabled>
|
||||
<AppShell.SidebarLink as={Link} href={routes.auth.strategies()}>
|
||||
Strategies
|
||||
</AppShell.SidebarLink>
|
||||
<AppShell.SidebarLink as={Link} href={routes.auth.settings()}>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import clsx from "clsx";
|
||||
import { TbChevronDown, TbChevronUp } from "react-icons/tb";
|
||||
import { isDebug } from "core";
|
||||
import { TbAlertCircle, TbChevronDown, TbChevronUp } from "react-icons/tb";
|
||||
import { useBknd } from "ui/client/BkndProvider";
|
||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { Icon } from "ui/components/display/Icon";
|
||||
import { Message } from "ui/components/display/Message";
|
||||
import { Field, type FieldProps, Form, Subscribe } from "ui/components/form/json-schema-form";
|
||||
import {
|
||||
Field,
|
||||
type FieldProps,
|
||||
Form,
|
||||
FormDebug,
|
||||
Subscribe
|
||||
} from "ui/components/form/json-schema-form";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
||||
@@ -40,12 +48,13 @@ export function AuthSettings(props) {
|
||||
|
||||
const formConfig = {
|
||||
ignoreKeys: ["roles", "strategies"],
|
||||
options: { keepEmpty: true }
|
||||
options: { keepEmpty: true, debug: isDebug() }
|
||||
};
|
||||
|
||||
function AuthSettingsInternal() {
|
||||
const { config, schema: _schema, actions } = useBkndAuth();
|
||||
const schema = JSON.parse(JSON.stringify(_schema));
|
||||
const hasRoles = Object.keys(config.roles ?? {}).length > 0;
|
||||
|
||||
schema.properties.jwt.required = ["alg"];
|
||||
|
||||
@@ -64,7 +73,7 @@ function AuthSettingsInternal() {
|
||||
>
|
||||
{({ dirty, errors, submitting }) => (
|
||||
<AppShell.SectionHeader
|
||||
className="pl-3"
|
||||
className="pl-4"
|
||||
right={
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -75,28 +84,32 @@ function AuthSettingsInternal() {
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<Breadcrumbs2
|
||||
path={[{ label: "Auth", href: "/" }, { label: "Settings" }]}
|
||||
backTo="/"
|
||||
/>
|
||||
</div>
|
||||
Settings
|
||||
</AppShell.SectionHeader>
|
||||
)}
|
||||
</Subscribe>
|
||||
<AppShell.Scrollable>
|
||||
<Section className="pt-4">
|
||||
<AuthField
|
||||
name="enabled"
|
||||
label="Authentication Enabled"
|
||||
description="Only after enabling authentication, all settings below will take effect."
|
||||
descriptionPlacement="top"
|
||||
/>
|
||||
<div className="flex flex-col gap-6 relative">
|
||||
<Section className="pt-4 pl-0 pb-0">
|
||||
<div className="pl-4">
|
||||
<AuthField
|
||||
name="enabled"
|
||||
label="Authentication Enabled"
|
||||
description="Only after enabling authentication, all settings below will take effect."
|
||||
descriptionPlacement="top"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 relative pl-4 pb-2">
|
||||
<Overlay />
|
||||
<AuthField
|
||||
name="guard.enabled"
|
||||
label="Guard Enabled"
|
||||
label={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span>Guard Enabled</span>
|
||||
{!hasRoles && (
|
||||
<Icon.Warning title="No roles defined. Enabling the guard will block all requests." />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
description="When enabled, enforces permissions on all routes. Make sure to create roles first."
|
||||
descriptionPlacement="top"
|
||||
/>
|
||||
@@ -139,7 +152,7 @@ function AuthSettingsInternal() {
|
||||
<ToggleAdvanced which="cookie" />
|
||||
</Section>
|
||||
</div>
|
||||
{/* <FormDebug /> */}
|
||||
<FormDebug />
|
||||
</AppShell.Scrollable>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -1,55 +1,247 @@
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
import { isDebug } from "core";
|
||||
import { autoFormatString } from "core/utils";
|
||||
import { type ChangeEvent, useState } from "react";
|
||||
import {
|
||||
TbAt,
|
||||
TbBrandAppleFilled,
|
||||
TbBrandDiscordFilled,
|
||||
TbBrandFacebookFilled,
|
||||
TbBrandGithubFilled,
|
||||
TbBrandGoogleFilled,
|
||||
TbBrandInstagram,
|
||||
TbBrandOauth,
|
||||
TbBrandX,
|
||||
TbSettings
|
||||
} from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Message } from "ui/components/display/Message";
|
||||
import {
|
||||
Field,
|
||||
Form,
|
||||
FormContextOverride,
|
||||
FormDebug,
|
||||
ObjectField,
|
||||
Subscribe,
|
||||
useDerivedFieldContext,
|
||||
useFormError,
|
||||
useFormValue
|
||||
} from "ui/components/form/json-schema-form";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
|
||||
export function AuthStrategiesList() {
|
||||
useBknd({ withSecrets: true });
|
||||
return <AuthStrategiesListInternal />;
|
||||
export function AuthStrategiesList(props) {
|
||||
useBrowserTitle(["Auth", "Strategies"]);
|
||||
|
||||
const { hasSecrets } = useBknd({ withSecrets: true });
|
||||
if (!hasSecrets) {
|
||||
return <Message.MissingPermission what="Auth Strategies" />;
|
||||
}
|
||||
|
||||
return <AuthStrategiesListInternal {...props} />;
|
||||
}
|
||||
|
||||
const uiSchema = {
|
||||
jwt: {
|
||||
fields: {
|
||||
"ui:options": {
|
||||
orderable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
const formOptions = {
|
||||
keepEmpty: true,
|
||||
debug: isDebug()
|
||||
};
|
||||
|
||||
function AuthStrategiesListInternal() {
|
||||
const s = useBknd();
|
||||
const config = s.config.auth.strategies;
|
||||
const schema = cloneDeep(omit(s.schema.auth.properties.strategies, ["title"]));
|
||||
const $auth = useBkndAuth();
|
||||
const config = $auth.config.strategies;
|
||||
const schema = $auth.schema.properties.strategies;
|
||||
const schemas = Object.fromEntries(
|
||||
// @ts-ignore
|
||||
$auth.schema.properties.strategies.additionalProperties.anyOf.map((s) => [
|
||||
s.properties.type.const,
|
||||
s
|
||||
])
|
||||
);
|
||||
|
||||
console.log("strategies", { config, schema });
|
||||
async function handleSubmit(data: any) {
|
||||
console.log("submit", { strategies: data });
|
||||
await $auth.actions.config.set({ strategies: data });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppShell.SectionHeader right={<Button variant="primary">Update</Button>}>
|
||||
Strategies
|
||||
</AppShell.SectionHeader>
|
||||
<Form schema={schema} initialValues={config} onSubmit={handleSubmit} options={formOptions}>
|
||||
<Subscribe
|
||||
selector={(state) => ({
|
||||
dirty: state.dirty,
|
||||
errors: state.errors.length > 0,
|
||||
submitting: state.submitting
|
||||
})}
|
||||
>
|
||||
{({ dirty, errors, submitting }) => (
|
||||
<AppShell.SectionHeader
|
||||
className="pl-4"
|
||||
right={
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={!dirty || errors || submitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
Strategies
|
||||
</AppShell.SectionHeader>
|
||||
)}
|
||||
</Subscribe>
|
||||
<AppShell.Scrollable>
|
||||
strat
|
||||
{/*<div className="flex flex-col flex-grow px-5 py-4 gap-8">
|
||||
<div>
|
||||
<JsonSchemaForm
|
||||
schema={generalSchema}
|
||||
className="legacy hide-required-mark fieldset-alternative mute-root"
|
||||
/>
|
||||
<div className="flex flex-col p-4 gap-4">
|
||||
<p className="opacity-70">
|
||||
Allow users to sign in or sign up using different strategies.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 max-w-4xl">
|
||||
<Strategy type="password" name="password" />
|
||||
<Strategy type="oauth" name="google" />
|
||||
<Strategy type="oauth" name="github" />
|
||||
<Strategy type="oauth" name="facebook" unavailable />
|
||||
<Strategy type="oauth" name="x" unavailable />
|
||||
<Strategy type="oauth" name="instagram" unavailable />
|
||||
<Strategy type="oauth" name="apple" unavailable />
|
||||
<Strategy type="oauth" name="discord" unavailable />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="font-bold">JWT Settings</h3>
|
||||
<JsonSchemaForm
|
||||
schema={extracted.jwt.schema}
|
||||
uiSchema={uiSchema.jwt}
|
||||
className="legacy hide-required-mark fieldset-alternative mute-root"
|
||||
/>
|
||||
</div>
|
||||
</div>*/}
|
||||
</div>
|
||||
<FormDebug />
|
||||
</AppShell.Scrollable>
|
||||
</>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
type StrategyProps = {
|
||||
type: "password" | "oauth" | "custom_oauth";
|
||||
name: string;
|
||||
unavailable?: boolean;
|
||||
};
|
||||
|
||||
const Strategy = ({ type, name, unavailable }: StrategyProps) => {
|
||||
const errors = useFormError(name, { strict: true });
|
||||
const $auth = useBkndAuth();
|
||||
const schemas = Object.fromEntries(
|
||||
// @ts-ignore
|
||||
$auth.schema.properties.strategies.additionalProperties.anyOf.map((s) => [
|
||||
s.properties.type.const,
|
||||
s
|
||||
])
|
||||
);
|
||||
const schema = schemas[type];
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (!schema) return null;
|
||||
|
||||
return (
|
||||
<FormContextOverride schema={schema} prefix={name}>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex flex-col border border-muted rounded bg-background",
|
||||
unavailable && "opacity-20 pointer-events-none cursor-not-allowed",
|
||||
errors.length > 0 && "border-red-500"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row justify-between p-3 gap-3 items-center">
|
||||
<div className="flex flex-row items-center p-2 bg-primary/5 rounded">
|
||||
<StrategyIcon type={type} provider={name} />
|
||||
</div>
|
||||
<div className="font-mono flex-grow flex flex-row gap-3">
|
||||
<span className="leading-none">{autoFormatString(name)}</span>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<StrategyToggle />
|
||||
<IconButton
|
||||
Icon={TbSettings}
|
||||
size="lg"
|
||||
iconProps={{ strokeWidth: 1.5 }}
|
||||
variant={open ? "primary" : "ghost"}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex flex-col border-t border-t-muted px-4 pt-3 pb-4 bg-lightest/50 gap-4"
|
||||
)}
|
||||
>
|
||||
<StrategyForm type={type} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FormContextOverride>
|
||||
);
|
||||
};
|
||||
|
||||
const StrategyToggle = () => {
|
||||
const ctx = useDerivedFieldContext("");
|
||||
const { value } = useFormValue("");
|
||||
|
||||
function handleToggleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
const checked = e.target.value;
|
||||
const value_keys = Object.keys(value ?? {});
|
||||
const can_remove =
|
||||
value_keys.length === 0 || (value_keys.length === 1 && value_keys[0] === "enabled");
|
||||
|
||||
if (!checked && can_remove) {
|
||||
ctx.deleteValue(ctx.path);
|
||||
} else {
|
||||
ctx.setValue([ctx.path, "enabled"].join("."), checked);
|
||||
}
|
||||
}
|
||||
|
||||
return <Field name="enabled" label={false} required onChange={handleToggleChange} />;
|
||||
};
|
||||
|
||||
const StrategyIcon = ({ type, provider }: { type: StrategyProps["type"]; provider?: string }) => {
|
||||
if (type === "password") {
|
||||
return <TbAt className="size-5" />;
|
||||
}
|
||||
|
||||
if (provider && provider in OAUTH_BRANDS) {
|
||||
const BrandIcon = OAUTH_BRANDS[provider];
|
||||
return <BrandIcon className="size-5" />;
|
||||
}
|
||||
|
||||
return <TbBrandOauth className="size-5" />;
|
||||
};
|
||||
|
||||
const OAUTH_BRANDS = {
|
||||
google: TbBrandGoogleFilled,
|
||||
github: TbBrandGithubFilled,
|
||||
facebook: TbBrandFacebookFilled,
|
||||
x: TbBrandX,
|
||||
instagram: TbBrandInstagram,
|
||||
apple: TbBrandAppleFilled,
|
||||
discord: TbBrandDiscordFilled
|
||||
};
|
||||
|
||||
const StrategyForm = ({ type }: Pick<StrategyProps, "type">) => {
|
||||
let Component = () => <ObjectField path="" wrapperProps={{ wrapper: "group", label: false }} />;
|
||||
switch (type) {
|
||||
case "password":
|
||||
Component = StrategyPasswordForm;
|
||||
break;
|
||||
case "oauth":
|
||||
Component = StrategyOAuthForm;
|
||||
break;
|
||||
}
|
||||
|
||||
return <Component />;
|
||||
};
|
||||
|
||||
const StrategyPasswordForm = () => {
|
||||
return <ObjectField path="config" wrapperProps={{ wrapper: "group", label: false }} />;
|
||||
};
|
||||
|
||||
const StrategyOAuthForm = () => {
|
||||
return (
|
||||
<>
|
||||
<Field name="config.client.client_id" required />
|
||||
<Field name="config.client.client_secret" required />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user