mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 21:06:04 +00:00
add auth enabling hints
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
import type { AppAuthSchema } from "auth/auth-schema";
|
import type { AppAuthSchema } from "auth/auth-schema";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
|
import { routes } from "ui/lib/routes";
|
||||||
|
|
||||||
export function useBkndAuth() {
|
export function useBkndAuth() {
|
||||||
const { config, schema, actions: bkndActions } = useBknd();
|
const { config, schema, actions: bkndActions, app } = useBknd();
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
config: {
|
config: {
|
||||||
@@ -33,7 +34,29 @@ export function useBkndAuth() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const $auth = {};
|
|
||||||
|
const minimum_permissions = [
|
||||||
|
"system.access.admin",
|
||||||
|
"system.access.api",
|
||||||
|
"system.config.read",
|
||||||
|
"system.config.read.secrets",
|
||||||
|
"system.build"
|
||||||
|
];
|
||||||
|
const $auth = {
|
||||||
|
roles: {
|
||||||
|
none: Object.keys(config.auth.roles ?? {}).length === 0,
|
||||||
|
minimum_permissions,
|
||||||
|
has_admin: Object.entries(config.auth.roles ?? {}).some(
|
||||||
|
([name, role]) =>
|
||||||
|
role.implicit_allow ||
|
||||||
|
minimum_permissions.every((p) => role.permissions?.includes(p))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
settings: app.getSettingsPath(["auth"]),
|
||||||
|
listUsers: app.getAbsolutePath("/data/" + routes.data.entity.list(config.auth.entity_name))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
$auth,
|
$auth,
|
||||||
|
|||||||
@@ -209,7 +209,8 @@ export const Switch = forwardRef<
|
|||||||
<RadixSwitch.Root
|
<RadixSwitch.Root
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative cursor-pointer rounded-full bg-muted border-2 border-transparent outline-none ring-1 dark:ring-primary/10 ring-primary/20 data-[state=checked]:ring-primary/60 data-[state=checked]:bg-primary/60 appearance-none transition-colors hover:bg-muted/80",
|
"relative cursor-pointer rounded-full bg-muted border-2 border-transparent outline-none ring-1 dark:ring-primary/10 ring-primary/20 data-[state=checked]:ring-primary/60 data-[state=checked]:bg-primary/60 appearance-none transition-colors hover:bg-muted/80",
|
||||||
SwitchSizes[props.size ?? "md"].root
|
SwitchSizes[props.size ?? "md"].root,
|
||||||
|
props.disabled && "opacity-50 !cursor-not-allowed"
|
||||||
)}
|
)}
|
||||||
onCheckedChange={(bool) => {
|
onCheckedChange={(bool) => {
|
||||||
console.log("setting", bool);
|
console.log("setting", bool);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { coerce, isType, isTypeSchema } from "./utils";
|
|||||||
export type FieldProps = {
|
export type FieldProps = {
|
||||||
onChange?: (e: ChangeEvent<any>) => void;
|
onChange?: (e: ChangeEvent<any>) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
} & Omit<FieldwrapperProps, "children" | "schema">;
|
} & Omit<FieldwrapperProps, "children" | "schema">;
|
||||||
|
|
||||||
export const Field = (props: FieldProps) => {
|
export const Field = (props: FieldProps) => {
|
||||||
@@ -49,7 +50,7 @@ const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props
|
|||||||
return <ArrayField path={name} />;
|
return <ArrayField path={name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabled = schema.readOnly ?? "const" in schema ?? false;
|
const disabled = props.disabled ?? schema.readOnly ?? "const" in schema ?? false;
|
||||||
|
|
||||||
const handleChange = useEvent((e: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useEvent((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = coerce(e.target.value, schema as any, { required });
|
const value = coerce(e.target.value, schema as any, { required });
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { IconFingerprint } from "@tabler/icons-react";
|
import { IconFingerprint } from "@tabler/icons-react";
|
||||||
import { TbSettings } from "react-icons/tb";
|
import { TbSettings } from "react-icons/tb";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { Empty } from "ui/components/display/Empty";
|
import { Empty } from "ui/components/display/Empty";
|
||||||
|
import { Icon } from "ui/components/display/Icon";
|
||||||
import { Link } from "ui/components/wouter/Link";
|
import { Link } from "ui/components/wouter/Link";
|
||||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { routes, useNavigate } from "ui/lib/routes";
|
import { routes } from "ui/lib/routes";
|
||||||
|
|
||||||
export function AuthRoot({ children }) {
|
export function AuthRoot({ children }) {
|
||||||
const { app, config } = useBknd();
|
const { config, $auth } = useBkndAuth();
|
||||||
const users_entity = config.auth.entity_name;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppShell.Sidebar>
|
<AppShell.Sidebar>
|
||||||
<AppShell.SectionHeader
|
<AppShell.SectionHeader
|
||||||
right={
|
right={
|
||||||
<Link href={app.getSettingsPath(["auth"])}>
|
<Link href={$auth.routes.settings}>
|
||||||
<IconButton Icon={TbSettings} />
|
<IconButton Icon={TbSettings} />
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
@@ -32,22 +32,42 @@ export function AuthRoot({ children }) {
|
|||||||
</AppShell.SidebarLink>
|
</AppShell.SidebarLink>
|
||||||
<AppShell.SidebarLink
|
<AppShell.SidebarLink
|
||||||
as={Link}
|
as={Link}
|
||||||
href={app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity))}
|
href={$auth.routes.listUsers}
|
||||||
disabled={!config.auth.enabled}
|
disabled={!config.enabled}
|
||||||
|
className="justify-between"
|
||||||
>
|
>
|
||||||
Users
|
Users
|
||||||
|
{!config.enabled && <AuthWarning title="Auth is not enabled." />}
|
||||||
</AppShell.SidebarLink>
|
</AppShell.SidebarLink>
|
||||||
<AppShell.SidebarLink
|
<AppShell.SidebarLink
|
||||||
as={Link}
|
as={Link}
|
||||||
href={routes.auth.roles.list()}
|
href={routes.auth.roles.list()}
|
||||||
disabled={!config.auth.enabled}
|
disabled={!config.enabled}
|
||||||
|
className="justify-between"
|
||||||
>
|
>
|
||||||
Roles & Permissions
|
Roles & Permissions
|
||||||
|
{!config.enabled ? (
|
||||||
|
<AuthWarning title="Auth is not enabled." />
|
||||||
|
) : $auth.roles.none ? (
|
||||||
|
<AuthWarning title="No roles defined." />
|
||||||
|
) : !$auth.roles.has_admin ? (
|
||||||
|
<AuthWarning title="No admin role defined." />
|
||||||
|
) : null}
|
||||||
</AppShell.SidebarLink>
|
</AppShell.SidebarLink>
|
||||||
<AppShell.SidebarLink as={Link} href={routes.auth.strategies()}>
|
<AppShell.SidebarLink
|
||||||
|
as={Link}
|
||||||
|
href={routes.auth.strategies()}
|
||||||
|
disabled={!config.enabled}
|
||||||
|
className="justify-between"
|
||||||
|
>
|
||||||
Strategies
|
Strategies
|
||||||
|
{!config.enabled && <AuthWarning title="Auth is not enabled." />}
|
||||||
</AppShell.SidebarLink>
|
</AppShell.SidebarLink>
|
||||||
<AppShell.SidebarLink as={Link} href={routes.auth.settings()}>
|
<AppShell.SidebarLink
|
||||||
|
as={Link}
|
||||||
|
href={routes.auth.settings()}
|
||||||
|
className="justify-between"
|
||||||
|
>
|
||||||
Settings
|
Settings
|
||||||
</AppShell.SidebarLink>
|
</AppShell.SidebarLink>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -59,6 +79,10 @@ export function AuthRoot({ children }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AuthWarning = ({ title }) => (
|
||||||
|
<Icon.Warning title={title} className="size-5 pointer-events-auto" />
|
||||||
|
);
|
||||||
|
|
||||||
export function AuthEmpty() {
|
export function AuthEmpty() {
|
||||||
useBrowserTitle(["Auth"]);
|
useBrowserTitle(["Auth"]);
|
||||||
return <Empty Icon={IconFingerprint} title="Not implemented yet" />;
|
return <Empty Icon={IconFingerprint} title="Not implemented yet" />;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function AuthIndex() {
|
|||||||
|
|
||||||
const usersLink = app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity));
|
const usersLink = app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity));
|
||||||
const rolesLink = routes.auth.roles.list();
|
const rolesLink = routes.auth.roles.list();
|
||||||
const strategiesLink = app.getSettingsPath(["auth", "strategies"]);
|
const strategiesLink = routes.auth.strategies();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -50,7 +50,7 @@ export function AuthIndex() {
|
|||||||
value={!enabled ? 0 : rolesTotal}
|
value={!enabled ? 0 : rolesTotal}
|
||||||
actions={[
|
actions={[
|
||||||
{ label: "View all", href: rolesLink },
|
{ label: "View all", href: rolesLink },
|
||||||
{ label: "Add new", variant: "default", href: rolesLink }
|
{ label: "Manage", variant: "default", href: rolesLink }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<KpiCard
|
<KpiCard
|
||||||
@@ -58,7 +58,7 @@ export function AuthIndex() {
|
|||||||
value={!enabled ? 0 : strategiesTotal}
|
value={!enabled ? 0 : strategiesTotal}
|
||||||
actions={[
|
actions={[
|
||||||
{ label: "View all", href: strategiesLink },
|
{ label: "View all", href: strategiesLink },
|
||||||
{ label: "Add new", variant: "default", href: strategiesLink }
|
{ label: "Manage", variant: "default", href: strategiesLink }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
} from "ui/components/form/json-schema-form";
|
} from "ui/components/form/json-schema-form";
|
||||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { combine } from "zustand/middleware";
|
import { combine } from "zustand/middleware";
|
||||||
|
|
||||||
@@ -52,9 +51,8 @@ const formConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function AuthSettingsInternal() {
|
function AuthSettingsInternal() {
|
||||||
const { config, schema: _schema, actions } = useBkndAuth();
|
const { config, schema: _schema, actions, $auth } = useBkndAuth();
|
||||||
const schema = JSON.parse(JSON.stringify(_schema));
|
const schema = JSON.parse(JSON.stringify(_schema));
|
||||||
const hasRoles = Object.keys(config.roles ?? {}).length > 0;
|
|
||||||
|
|
||||||
schema.properties.jwt.required = ["alg"];
|
schema.properties.jwt.required = ["alg"];
|
||||||
|
|
||||||
@@ -105,11 +103,12 @@ function AuthSettingsInternal() {
|
|||||||
label={
|
label={
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<span>Guard Enabled</span>
|
<span>Guard Enabled</span>
|
||||||
{!hasRoles && (
|
{!$auth.roles.has_admin && (
|
||||||
<Icon.Warning title="No roles defined. Enabling the guard will block all requests." />
|
<Icon.Warning title="No admin roles defined. Enabling the guard will likely block all requests." />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
disabled={$auth.roles.none}
|
||||||
description="When enabled, enforces permissions on all routes. Make sure to create roles first."
|
description="When enabled, enforces permissions on all routes. Make sure to create roles first."
|
||||||
descriptionPlacement="top"
|
descriptionPlacement="top"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user