mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
public commit
This commit is contained in:
60
app/src/ui/routes/auth/_auth.root.tsx
Normal file
60
app/src/ui/routes/auth/_auth.root.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { IconFingerprint } from "@tabler/icons-react";
|
||||
import { TbSettings } from "react-icons/tb";
|
||||
import { useBknd } from "ui/client";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { routes, useNavigate } from "ui/lib/routes";
|
||||
|
||||
export function AuthRoot({ children }) {
|
||||
const { app, config } = useBknd();
|
||||
const users_entity = config.auth.entity_name;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppShell.Sidebar>
|
||||
<AppShell.SectionHeader
|
||||
right={
|
||||
<Link href={app.getSettingsPath(["auth"])}>
|
||||
<IconButton Icon={TbSettings} />
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
Auth
|
||||
</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable initialOffset={96}>
|
||||
<div className="flex flex-col flex-grow p-3 gap-3">
|
||||
<nav className="flex flex-col flex-1 gap-1">
|
||||
<AppShell.SidebarLink as={Link} href={"/"}>
|
||||
Overview
|
||||
</AppShell.SidebarLink>
|
||||
<AppShell.SidebarLink
|
||||
as={Link}
|
||||
href={app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity))}
|
||||
>
|
||||
Users
|
||||
</AppShell.SidebarLink>
|
||||
<AppShell.SidebarLink as={Link} href={routes.auth.roles.list()}>
|
||||
Roles & Permissions
|
||||
</AppShell.SidebarLink>
|
||||
<AppShell.SidebarLink as={Link} href={routes.auth.strategies()} disabled>
|
||||
Strategies
|
||||
</AppShell.SidebarLink>
|
||||
<AppShell.SidebarLink as={Link} href={routes.auth.settings()}>
|
||||
Settings
|
||||
</AppShell.SidebarLink>
|
||||
</nav>
|
||||
</div>
|
||||
</AppShell.Scrollable>
|
||||
</AppShell.Sidebar>
|
||||
<AppShell.Main>{children}</AppShell.Main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthEmpty() {
|
||||
useBrowserTitle(["Auth"]);
|
||||
return <Empty Icon={IconFingerprint} title="Not implemented yet" />;
|
||||
}
|
||||
92
app/src/ui/routes/auth/auth.index.tsx
Normal file
92
app/src/ui/routes/auth/auth.index.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useBknd, useClient } from "ui/client";
|
||||
import { routes } from "ui/lib/routes";
|
||||
import {
|
||||
Button,
|
||||
ButtonLink,
|
||||
type ButtonLinkProps,
|
||||
type ButtonProps
|
||||
} from "../../components/buttons/Button";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
|
||||
export function AuthIndex() {
|
||||
const client = useClient();
|
||||
const { app, config } = useBknd();
|
||||
const users_entity = config.auth.entity_name;
|
||||
const query = client.query().data.entity("users").count();
|
||||
const usersTotal = query.data?.body.count ?? 0;
|
||||
const {
|
||||
config: {
|
||||
auth: { roles, strategies }
|
||||
}
|
||||
} = useBknd();
|
||||
const rolesTotal = Object.keys(roles ?? {}).length ?? 0;
|
||||
const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0;
|
||||
|
||||
const usersLink = app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity));
|
||||
const rolesLink = routes.auth.roles.list();
|
||||
const strategiesLink = app.getSettingsPath(["auth", "strategies"]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppShell.SectionHeader>Overview</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable>
|
||||
<div className="flex flex-col flex-grow p-3 gap-3">
|
||||
<div className="grid xs:grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-5">
|
||||
<KpiCard
|
||||
title="Users registered"
|
||||
value={usersTotal}
|
||||
actions={[
|
||||
{
|
||||
label: "View all",
|
||||
href: usersLink
|
||||
},
|
||||
{ label: "Add new", variant: "default", href: usersLink }
|
||||
]}
|
||||
/>
|
||||
<KpiCard
|
||||
title="Roles"
|
||||
value={rolesTotal}
|
||||
actions={[
|
||||
{ label: "View all", href: rolesLink },
|
||||
{ label: "Add new", variant: "default", href: rolesLink }
|
||||
]}
|
||||
/>
|
||||
<KpiCard
|
||||
title="Strategies enabled"
|
||||
value={strategiesTotal}
|
||||
actions={[
|
||||
{ label: "View all", href: strategiesLink },
|
||||
{ label: "Add new", variant: "default", href: strategiesLink }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell.Scrollable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type KpiCardProps = {
|
||||
title: string;
|
||||
value: number;
|
||||
actions: (Omit<ButtonLinkProps, "href"> & { label: string; href?: string })[];
|
||||
};
|
||||
|
||||
const KpiCard: React.FC<KpiCardProps> = ({ title, value, actions }) => (
|
||||
<div className="flex flex-col border border-muted">
|
||||
<div className="flex flex-col gap-2 px-5 pt-3.5 pb-4 border-b border-b-muted">
|
||||
<div>
|
||||
<span className="opacity-50">{title}</span>
|
||||
{/*<span>+6.1%</span>*/}
|
||||
</div>
|
||||
<div className="text-4xl font-medium">{value}</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3 p-3 justify-between">
|
||||
{actions.map((action, i) => (
|
||||
<ButtonLink key={i} size="small" variant="ghost" href="#" {...action}>
|
||||
{action.label}
|
||||
</ButtonLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
121
app/src/ui/routes/auth/auth.login.tsx
Normal file
121
app/src/ui/routes/auth/auth.login.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { AppAuthOAuthStrategy } from "auth/auth-schema";
|
||||
import { Type, ucFirst, ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
|
||||
import { transform } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuth } from "ui/client";
|
||||
import { useAuthStrategies } from "ui/client/schema/auth/use-auth";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { Logo } from "ui/components/display/Logo";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import { useSearch } from "ui/hooks/use-search";
|
||||
import { LoginForm } from "ui/modules/auth/LoginForm";
|
||||
import { useLocation } from "wouter";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
|
||||
const schema = Type.Object({
|
||||
token: Type.String()
|
||||
});
|
||||
|
||||
export function AuthLogin() {
|
||||
useBrowserTitle(["Login"]);
|
||||
const [, navigate] = useLocation();
|
||||
const search = useSearch(schema);
|
||||
const token = search.value.token;
|
||||
//console.log("search", token, "/api/auth/google?redirect=" + window.location.href);
|
||||
|
||||
const auth = useAuth();
|
||||
const { strategies, loading } = useAuthStrategies();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
auth.setToken(token);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
async function handleSubmit(value: { email: string; password: string }) {
|
||||
console.log("submit", value);
|
||||
const { res, data } = await auth.login(value);
|
||||
if (!res.ok) {
|
||||
if (data && "error" in data) {
|
||||
setError(data.error.message);
|
||||
} else {
|
||||
setError("An error occurred");
|
||||
}
|
||||
} else if (error) {
|
||||
setError(null);
|
||||
}
|
||||
console.log("res:login", { res, data });
|
||||
}
|
||||
|
||||
if (auth.user) {
|
||||
console.log("user set", auth.user);
|
||||
navigate("/", { replace: true });
|
||||
}
|
||||
|
||||
const oauth = transform(
|
||||
strategies ?? {},
|
||||
(result, value, key) => {
|
||||
if (value.type !== "password") {
|
||||
result[key] = value.config;
|
||||
}
|
||||
},
|
||||
{}
|
||||
) as Record<string, AppAuthOAuthStrategy>;
|
||||
console.log("oauth", oauth, strategies);
|
||||
|
||||
return (
|
||||
<AppShell.Root>
|
||||
<AppShell.Content center>
|
||||
{!loading && (
|
||||
<div className="flex flex-col gap-4 items-center w-96 px-6 py-7">
|
||||
<Link href={"/"} className="link">
|
||||
<Logo scale={0.25} />
|
||||
</Link>
|
||||
<div className="flex flex-col items-center">
|
||||
<h1 className="text-xl font-bold">Sign in to your admin panel</h1>
|
||||
<p className="text-primary/50">Enter your credentials below to get access.</p>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="bg-red-500/40 p-3 w-full rounded font-bold mb-1">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{Object.keys(oauth).length > 0 && (
|
||||
<>
|
||||
{Object.entries(oauth)?.map(([name, oauth], key) => (
|
||||
<Button
|
||||
key={key}
|
||||
size="large"
|
||||
variant="outline"
|
||||
className="justify-center"
|
||||
onClick={() => {
|
||||
window.location.href = `/api/auth/${name}/login?redirect=${window.location.href}`;
|
||||
}}
|
||||
>
|
||||
Continue with {ucFirstAllSnakeToPascalWithSpaces(oauth.name)}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div className="w-full flex flex-row items-center">
|
||||
<div className="relative flex grow">
|
||||
<div className="h-px bg-primary/10 w-full absolute top-[50%] z-0" />
|
||||
</div>
|
||||
<div className="mx-5">or</div>
|
||||
<div className="relative flex grow">
|
||||
<div className="h-px bg-primary/10 w-full absolute top-[50%] z-0" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<LoginForm onSubmitted={handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AppShell.Content>
|
||||
</AppShell.Root>
|
||||
);
|
||||
}
|
||||
90
app/src/ui/routes/auth/auth.roles.edit.$role.tsx
Normal file
90
app/src/ui/routes/auth/auth.roles.edit.$role.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useRef } from "react";
|
||||
import { TbDots } from "react-icons/tb";
|
||||
import { useBknd } from "ui/client";
|
||||
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 { Dropdown } from "ui/components/overlay/Dropdown";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
||||
import { routes, useNavigate } from "ui/lib/routes";
|
||||
import { AuthRoleForm, type AuthRoleFormRef } from "ui/routes/auth/forms/role.form";
|
||||
|
||||
export function AuthRolesEdit(props) {
|
||||
useBknd({ withSecrets: true });
|
||||
return <AuthRolesEditInternal {...props} />;
|
||||
}
|
||||
|
||||
function AuthRolesEditInternal({ params }) {
|
||||
const [navigate] = useNavigate();
|
||||
const { config, actions } = useBkndAuth();
|
||||
const roleName = params.role;
|
||||
const role = config.roles?.[roleName];
|
||||
const formRef = useRef<AuthRoleFormRef>(null);
|
||||
|
||||
async function handleUpdate() {
|
||||
console.log("data", formRef.current?.isValid());
|
||||
if (!formRef.current?.isValid()) return;
|
||||
const data = formRef.current?.getData();
|
||||
const success = await actions.roles.patch(roleName, data);
|
||||
|
||||
notifications.show({
|
||||
id: `role-${roleName}-update`,
|
||||
position: "top-right",
|
||||
title: success ? "Update success" : "Update failed",
|
||||
message: success ? "Role updated successfully" : "Failed to update role",
|
||||
color: !success ? "red" : undefined
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (await actions.roles.delete(roleName)) {
|
||||
navigate(routes.auth.roles.list());
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppShell.SectionHeader
|
||||
right={
|
||||
<>
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: "Advanced Settings",
|
||||
onClick: () =>
|
||||
navigate(routes.settings.path(["auth", "roles", roleName]), {
|
||||
absolute: true
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
onClick: handleDelete,
|
||||
destructive: true
|
||||
}
|
||||
]}
|
||||
position="bottom-end"
|
||||
>
|
||||
<IconButton Icon={TbDots} />
|
||||
</Dropdown>
|
||||
<Button variant="primary" onClick={handleUpdate}>
|
||||
Update
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
className="pl-3"
|
||||
>
|
||||
<Breadcrumbs2
|
||||
path={[
|
||||
{ label: "Roles & Permissions", href: routes.auth.roles.list() },
|
||||
{ label: roleName }
|
||||
]}
|
||||
/>
|
||||
</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable>
|
||||
<AuthRoleForm ref={formRef} role={role} />
|
||||
</AppShell.Scrollable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
128
app/src/ui/routes/auth/auth.roles.tsx
Normal file
128
app/src/ui/routes/auth/auth.roles.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Modal, TextInput } from "@mantine/core";
|
||||
import { useDisclosure, useFocusTrap } from "@mantine/hooks";
|
||||
import { StringIdentifier, transformObject, ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
|
||||
import { useRef } from "react";
|
||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import { bkndModals } from "ui/modals";
|
||||
import { SchemaFormModal } from "ui/modals/debug/SchemaFormModal";
|
||||
import { useBknd } from "../../client/BkndProvider";
|
||||
import { Button } from "../../components/buttons/Button";
|
||||
import { CellValue, DataTable } from "../../components/table/DataTable";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
import { routes, useNavigate } from "../../lib/routes";
|
||||
|
||||
export function AuthRolesList() {
|
||||
const [navigate] = useNavigate();
|
||||
const [modalOpen, modalHandler] = useDisclosure(false);
|
||||
const focusRef = useFocusTrap();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { config, actions } = useBkndAuth();
|
||||
|
||||
const data = Object.values(
|
||||
transformObject(config.roles ?? {}, (role, name) => ({
|
||||
role: name,
|
||||
permissions: role.permissions,
|
||||
is_default: role.is_default ?? false,
|
||||
implicit_allow: role.implicit_allow ?? false
|
||||
}))
|
||||
);
|
||||
|
||||
function handleClick(row) {
|
||||
navigate(routes.auth.roles.edit(row.role));
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
bkndModals.open(
|
||||
"form",
|
||||
{
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: StringIdentifier
|
||||
},
|
||||
required: ["name"]
|
||||
},
|
||||
uiSchema: {
|
||||
name: {
|
||||
"ui:title": "Role name"
|
||||
}
|
||||
},
|
||||
onSubmit: async (data) => {
|
||||
if (data.name.length > 0) {
|
||||
if (await actions.roles.add(data.name)) {
|
||||
navigate(routes.auth.roles.edit(data.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "New Role"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*<Modal
|
||||
ref={focusRef}
|
||||
opened={modalOpen}
|
||||
onClose={modalHandler.close}
|
||||
title={"New Role"}
|
||||
classNames={{
|
||||
root: "bknd-admin",
|
||||
header: "!bg-primary/5 border-b border-b-muted !py-3 px-5 !h-auto !min-h-px",
|
||||
content: "rounded-lg select-none",
|
||||
title: "font-bold !text-md",
|
||||
body: "pt-3 pb-3 px-3 gap-4 flex flex-col"
|
||||
}}
|
||||
>
|
||||
<TextInput ref={inputRef} data-autofocus size="md" placeholder="Enter role name" />
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
<Button onClick={() => modalHandler.close()}>Cancel</Button>
|
||||
<Button variant="primary" onClick={handleClickAdd}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>*/}
|
||||
<AppShell.SectionHeader
|
||||
right={
|
||||
<Button variant="primary" onClick={openCreateModal}>
|
||||
Create new
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
Roles & Permissions
|
||||
</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable>
|
||||
<div className="flex flex-col flex-grow p-3 gap-3">
|
||||
<DataTable
|
||||
data={data}
|
||||
renderValue={renderValue}
|
||||
renderHeader={ucFirstAllSnakeToPascalWithSpaces}
|
||||
onClickRow={handleClick}
|
||||
/>
|
||||
</div>
|
||||
</AppShell.Scrollable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const renderValue = ({ value, property }) => {
|
||||
if (["is_default", "implicit_allow"].includes(property)) {
|
||||
return value ? <span>Yes</span> : <span className="opacity-50">No</span>;
|
||||
}
|
||||
|
||||
if (property === "permissions") {
|
||||
return [...(value || [])].map((p, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-block px-2 py-1.5 text-sm bg-primary/5 rounded font-mono leading-none"
|
||||
>
|
||||
{p}
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
||||
return <CellValue value={value} property={property} />;
|
||||
};
|
||||
79
app/src/ui/routes/auth/auth.settings.tsx
Normal file
79
app/src/ui/routes/auth/auth.settings.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
import { useBknd } from "ui/client";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { extractSchema } from "../settings/utils/schema";
|
||||
|
||||
export function AuthSettingsList() {
|
||||
useBknd({ withSecrets: true });
|
||||
return <AuthSettingsListInternal />;
|
||||
}
|
||||
|
||||
const uiSchema = {
|
||||
jwt: {
|
||||
fields: {
|
||||
"ui:options": {
|
||||
orderable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function AuthSettingsListInternal() {
|
||||
const s = useBknd();
|
||||
const config = s.config.auth;
|
||||
const schema = cloneDeep(omit(s.schema.auth, ["title"]));
|
||||
const [generalSchema, generalConfig, extracted] = extractSchema(schema as any, config, [
|
||||
"jwt",
|
||||
"roles",
|
||||
"guard",
|
||||
"strategies"
|
||||
]);
|
||||
try {
|
||||
const user_entity = config.entity_name ?? "users";
|
||||
const entities = s.config.data.entities ?? {};
|
||||
const user_fields = Object.entries(entities[user_entity]?.fields ?? {})
|
||||
.map(([name, field]) => (!field.config?.virtual ? name : undefined))
|
||||
.filter(Boolean);
|
||||
|
||||
if (user_fields) {
|
||||
console.log("user_fields", user_fields);
|
||||
extracted.jwt.schema.properties.fields.items.enum = user_fields;
|
||||
extracted.jwt.schema.properties.fields.uniqueItems = true;
|
||||
uiSchema.jwt.fields["ui:widget"] = "checkboxes";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
console.log({ generalSchema, generalConfig, extracted });
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppShell.SectionHeader right={<Button variant="primary">Update</Button>}>
|
||||
Settings
|
||||
</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</AppShell.Scrollable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthJwtSettings() {}
|
||||
57
app/src/ui/routes/auth/auth.strategies.tsx
Normal file
57
app/src/ui/routes/auth/auth.strategies.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
import { useBknd } from "ui/client";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
import { extractSchema } from "../settings/utils/schema";
|
||||
|
||||
export function AuthStrategiesList() {
|
||||
useBknd({ withSecrets: true });
|
||||
return <AuthStrategiesListInternal />;
|
||||
}
|
||||
|
||||
const uiSchema = {
|
||||
jwt: {
|
||||
fields: {
|
||||
"ui:options": {
|
||||
orderable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function AuthStrategiesListInternal() {
|
||||
const s = useBknd();
|
||||
const config = s.config.auth.strategies;
|
||||
const schema = cloneDeep(omit(s.schema.auth.properties.strategies, ["title"]));
|
||||
|
||||
console.log("strategies", { config, schema });
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppShell.SectionHeader right={<Button variant="primary">Update</Button>}>
|
||||
Strategies
|
||||
</AppShell.SectionHeader>
|
||||
<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>
|
||||
|
||||
<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>*/}
|
||||
</AppShell.Scrollable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
app/src/ui/routes/auth/auth.users.tsx
Normal file
3
app/src/ui/routes/auth/auth.users.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function AuthUsersList() {
|
||||
return null;
|
||||
}
|
||||
152
app/src/ui/routes/auth/forms/role.form.tsx
Normal file
152
app/src/ui/routes/auth/forms/role.form.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { typeboxResolver } from "@hookform/resolvers/typebox";
|
||||
import { Input, Switch, Tooltip } from "@mantine/core";
|
||||
import { guardRoleSchema } from "auth/auth-schema";
|
||||
import { type Static, ucFirst } from "core/utils";
|
||||
import type { TAppDataEntityFields } from "data/data-schema";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import { type UseControllerProps, useController, useForm } from "react-hook-form";
|
||||
import { Button, useBknd } from "ui";
|
||||
import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch";
|
||||
|
||||
const schema = guardRoleSchema;
|
||||
type Role = Static<typeof guardRoleSchema>;
|
||||
|
||||
export type AuthRoleFormRef = {
|
||||
getData: () => Role;
|
||||
isValid: () => boolean;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const AuthRoleForm = forwardRef<
|
||||
AuthRoleFormRef,
|
||||
{
|
||||
role?: Role;
|
||||
debug?: boolean;
|
||||
}
|
||||
>(({ role, debug }, ref) => {
|
||||
const { permissions } = useBknd();
|
||||
|
||||
const {
|
||||
formState: { isValid },
|
||||
watch,
|
||||
control,
|
||||
reset,
|
||||
getValues
|
||||
} = useForm({
|
||||
resolver: typeboxResolver(schema),
|
||||
defaultValues: role
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset,
|
||||
getData: () => getValues(),
|
||||
isValid: () => isValid
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-grow px-5 py-5 gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
{/*<h3 className="font-semibold">Role Permissions</h3>*/}
|
||||
<Permissions control={control} permissions={permissions} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input.Wrapper
|
||||
label="Should this role be the default?"
|
||||
size="md"
|
||||
description="In case an user is not assigned any role, this role will be assigned by default."
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<MantineSwitch name="is_default" control={control} className="mt-2" />
|
||||
</div>
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label="Implicit allow missing permissions?"
|
||||
size="md"
|
||||
description="This should be only used for admins. If a permission is not explicitly denied, it will be allowed."
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<MantineSwitch name="implicit_allow" control={control} className="mt-2" />
|
||||
</div>
|
||||
</Input.Wrapper>
|
||||
</div>
|
||||
|
||||
{debug && (
|
||||
<div className="font-mono opacity-50">
|
||||
<div>{JSON.stringify(role, null, 2)}</div>
|
||||
<div>{JSON.stringify(watch(), null, 2)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Permissions = ({
|
||||
control,
|
||||
permissions
|
||||
}: Omit<UseControllerProps, "name"> & { permissions: string[] }) => {
|
||||
const {
|
||||
field: { value, onChange: fieldOnChange, ...field },
|
||||
fieldState
|
||||
} = useController<Static<typeof schema>, "permissions">({
|
||||
name: "permissions",
|
||||
control
|
||||
});
|
||||
const data = value ?? [];
|
||||
|
||||
function handleChange(permission: string) {
|
||||
return (e) => {
|
||||
const checked = e.target.checked;
|
||||
const newValue = checked ? [...data, permission] : data.filter((p) => p !== permission);
|
||||
fieldOnChange(newValue);
|
||||
};
|
||||
}
|
||||
|
||||
const grouped = permissions.reduce(
|
||||
(acc, permission) => {
|
||||
const [group, name] = permission.split(".") as [string, string];
|
||||
if (!acc[group]) acc[group] = [];
|
||||
acc[group].push(permission);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string[]>
|
||||
);
|
||||
|
||||
console.log("grouped", grouped);
|
||||
//console.log("fieldState", fieldState, value);
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
{Object.entries(grouped).map(([group, permissions]) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2" key={group}>
|
||||
<h3 className="font-semibold">{ucFirst(group)} Permissions</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-3">
|
||||
{permissions.map((permission) => {
|
||||
const selected = data.includes(permission);
|
||||
return (
|
||||
<div key={permission} className="flex flex-col border border-muted">
|
||||
<div className="flex flex-row gap-2 justify-between">
|
||||
<div className="py-4 px-4 font-mono leading-none">
|
||||
{permission}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 items-center px-2">
|
||||
<Switch
|
||||
checked={selected}
|
||||
onChange={handleChange(permission)}
|
||||
/>
|
||||
<Tooltip label="Coming soon">
|
||||
<Button size="small" variant="ghost" disabled>
|
||||
<span className="font-normal italic font-mono">FX</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
app/src/ui/routes/auth/index.tsx
Normal file
21
app/src/ui/routes/auth/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Route } from "wouter";
|
||||
import { AuthRoot } from "./_auth.root";
|
||||
import { AuthIndex } from "./auth.index";
|
||||
import { AuthRolesList } from "./auth.roles";
|
||||
import { AuthRolesEdit } from "./auth.roles.edit.$role";
|
||||
import { AuthSettingsList } from "./auth.settings";
|
||||
import { AuthStrategiesList } from "./auth.strategies";
|
||||
import { AuthUsersList } from "./auth.users";
|
||||
|
||||
export default function AuthRoutes() {
|
||||
return (
|
||||
<AuthRoot>
|
||||
<Route path="/" component={AuthIndex} />
|
||||
<Route path="/users" component={AuthUsersList} />
|
||||
<Route path="/roles" component={AuthRolesList} />
|
||||
<Route path="/roles/edit/:role" component={AuthRolesEdit} />
|
||||
<Route path="/strategies" component={AuthStrategiesList} />
|
||||
<Route path="/settings" component={AuthSettingsList} />
|
||||
</AuthRoot>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user