import { useBknd } from "ui/client/bknd"; import { Message } from "ui/components/display/Message"; import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth"; import { useBrowserTitle } from "ui/hooks/use-browser-title"; import { useState } from "react"; import { useNavigate } from "ui/lib/routes"; import { isDebug } from "core/env"; import { Dropdown } from "ui/components/overlay/Dropdown"; import { IconButton } from "ui/components/buttons/IconButton"; import { TbAdjustments, TbDots, TbFilter, TbTrash, TbInfoCircle, TbCodeDots } from "react-icons/tb"; import { Button } from "ui/components/buttons/Button"; import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2"; import { routes } from "ui/lib/routes"; import * as AppShell from "ui/layouts/AppShell/AppShell"; import * as Formy from "ui/components/form/Formy"; import { ucFirst, s, transformObject, isObject, autoFormatString } from "bknd/utils"; import type { ModuleSchemas } from "bknd"; import { CustomField, Field, FieldWrapper, Form, FormContextOverride, FormDebug, ObjectJsonField, Subscribe, useDerivedFieldContext, useFormContext, useFormError, useFormValue, } from "ui/components/form/json-schema-form"; import type { TPermission } from "auth/authorize/Permission"; import type { RoleSchema } from "auth/authorize/Role"; import { SegmentedControl, Tooltip } from "@mantine/core"; import { Popover } from "ui/components/overlay/Popover"; import { cn } from "ui/lib/utils"; import { JsonViewer } from "ui/components/code/JsonViewer"; import { mountOnce, useApiQuery } from "ui/client"; import { CodePreview } from "ui/components/code/CodePreview"; import type { JsonError } from "json-schema-library"; import { Alert } from "ui/components/display/Alert"; export function AuthRolesEdit(props) { useBrowserTitle(["Auth", "Roles", props.params.role]); const { hasSecrets } = useBknd({ withSecrets: true }); if (!hasSecrets) { return ; } return ; } // currently for backward compatibility function getSchema(authSchema: ModuleSchemas["auth"]) { const roles = authSchema.properties.roles.additionalProperties; return { ...roles, properties: { ...roles.properties, permissions: { ...roles.properties.permissions.anyOf[1], }, }, }; } const formConfig = { options: { debug: isDebug(), }, }; function AuthRolesEditInternal({ params }: { params: { role: string } }) { const [navigate] = useNavigate(); const { config, schema: authSchema, actions } = useBkndAuth(); const [error, setError] = useState(); const roleName = params.role; const role = config.roles?.[roleName]; const { readonly, permissions } = useBknd(); const schema = getSchema(authSchema); const data = { ...role, // this is to maintain array structure permissions: permissions.map((p) => { return role?.permissions?.find((v: any) => v.permission === p.name); }), }; async function handleDelete() { const success = await actions.roles.delete(roleName); if (success) { navigate(routes.auth.roles.list()); } } async function handleUpdate(data: any) { setError(undefined); await actions.roles.patch(roleName, data); } return (
{ return { ...data, permissions: [...Object.values(data.permissions).filter(Boolean)], }; }} onSubmit={handleUpdate} onInvalidSubmit={(errors) => { setError(errors); }} > navigate(routes.settings.path(["auth", "roles", roleName]), { absolute: true, }), }, !readonly && { label: "Delete", onClick: handleDelete, destructive: true, }, ]} position="bottom-end" > {!readonly && ( ({ dirty: state.dirty, errors: state.errors.length > 0, submitting: state.submitting, })} > {({ dirty, errors, submitting }) => ( )} )} } className="pl-3" > {error && }
); } type PermissionsData = Exclude; type PermissionData = PermissionsData[number]; const Permissions = () => { const { permissions } = useBknd(); const grouped = permissions.reduce( (acc, permission, index) => { const [group, name] = permission.name.split(".") as [string, string]; if (!acc[group]) acc[group] = []; acc[group].push({ index, permission }); return acc; }, {} as Record, ); return (
{Object.entries(grouped).map(([group, rows]) => { return (

{ucFirst(group)} Permissions

{rows.map(({ index, permission }) => ( ))}
); })}
); }; const Permission = ({ permission, index }: { permission: TPermission; index?: number }) => { const path = `permissions.${index}`; const { value } = useDerivedFieldContext("permissions", (ctx) => { const v = ctx.value; if (!Array.isArray(v)) return undefined; const v2 = v.find((v) => v && v.permission === permission.name); return { set: !!v2, policies: (v2?.policies?.length ?? 0) as number, }; }); const { setValue } = useFormContext(); const [open, setOpen] = useState(false); const policiesCount = value?.policies ?? 0; const isSet = value?.set ?? false; async function handleSwitch() { if (isSet) { setValue(path, undefined); setOpen(false); } else { setValue(path, { permission: permission.name, policies: [], effect: "allow", }); } } function toggleOpen() { setOpen((o) => !o); } return ( <>
{permission.name} {permission.filterable && ( )}
{policiesCount > 0 && (
{policiesCount}
)}
{open && (
)}
); }; const Policies = ({ path, permission }: { path: string; permission: TPermission }) => { const { setValue, schema: policySchema, lib, deleteValue, value, } = useDerivedFieldContext(path, ({ value }) => { return { policies: (value?.length ?? 0) as number, }; }); const policiesCount = value?.policies ?? 0; function handleAdd() { setValue( `${path}.${policiesCount}`, lib.getTemplate(undefined, policySchema!.items, { addOptionalProps: true, }), ); } function handleDelete(index: number) { deleteValue(`${path}.${index}`); } return (
0 && "gap-8")}>
{policiesCount > 0 && Array.from({ length: policiesCount }).map((_, i) => ( {i > 0 &&
}
handleDelete(i)} size="sm" />
))}
); }; const mergeSchemas = (...schemas: object[]) => { return s.allOf(schemas.filter(Boolean).map(s.fromSchema)); }; function replaceEntitiesEnum(schema: Record, entities: string[]) { if (!isObject(schema) || !Array.isArray(entities) || entities.length === 0) return schema; return transformObject(schema, (sub, name) => { if (name === "properties") { return transformObject(sub as Record, (propConfig, propKey) => { if (propKey === "entity" && propConfig.type === "string") { return { ...propConfig, enum: entities, }; } return propConfig; }); } return sub; }); } const Policy = ({ permission, }: { permission: TPermission; }) => { const { value } = useDerivedFieldContext("", ({ value }) => ({ effect: (value?.effect ?? "allow") as "allow" | "deny" | "filter", })); const $bknd = useBknd(); const $permissions = useApiQuery((api) => api.system.permissions(), { use: [mountOnce], }); const entities = Object.keys($bknd.config.data.entities ?? {}); const ctx = $permissions.data ? mergeSchemas( $permissions.data.context, replaceEntitiesEnum(permission.context ?? null, entities), ) : undefined; return (
{({ value, setValue }) => ( setValue(value)} data={ ["allow", "deny", permission.filterable ? "filter" : undefined] .filter(Boolean) .map((effect) => ({ label: ucFirst(effect ?? ""), value: effect, })) as any } /> )} {value?.effect === "filter" && ( )}
); }; const CustomFieldWrapper = ({ children, name, label, description, schema, }: { children: React.ReactNode; name: string; label: string; description: string; schema?: { name: string; content: string | object; }; }) => { const errors = useFormError(name, { strict: true }); const Errors = errors.length > 0 && ( {errors.map((e) => e.message).join(", ")} ); return (
{label} {description && ( )}
{schema && (
typeof schema.content === "object" ? ( ) : ( ) } >
)}
{children} {Errors}
); };