add auth enabling hints

This commit is contained in:
dswbx
2025-02-26 18:44:16 +01:00
parent 2a9c1be151
commit 9a683c8e35
6 changed files with 70 additions and 22 deletions

View File

@@ -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,

View File

@@ -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);

View File

@@ -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 });

View File

@@ -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" />;

View File

@@ -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>

View File

@@ -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"
/> />