feat: add integration access settings (#725)
* feat: add integration access settings * fix: typecheck and test issues * fix: test timeout * chore: address pull request feedback * chore: add throw if action forbidden for integration permissions * fix: unable to create new migrations because of duplicate prevId in sqlite snapshots * chore: add sqlite migration for integration permissions * test: add unit tests for integration access * test: add permission checks to integration router tests * test: add unit test for integration permissions * chore: add mysql migration * fix: format issues
This commit is contained in:
189
apps/nextjs/src/components/access/access-settings.tsx
Normal file
189
apps/nextjs/src/components/access/access-settings.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { useState } from "react";
|
||||
import { Group, Stack, Tabs } from "@mantine/core";
|
||||
import { IconUser, IconUserDown, IconUsersGroup } from "@tabler/icons-react";
|
||||
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
import { CountBadge } from "@homarr/ui";
|
||||
|
||||
import { AccessProvider } from "./context";
|
||||
import type { AccessFormType } from "./form";
|
||||
import { GroupAccessForm } from "./group-access-form";
|
||||
import { InheritAccessTable } from "./inherit-access-table";
|
||||
import { UsersAccessForm } from "./user-access-form";
|
||||
|
||||
interface GroupAccessPermission<TPermission extends string> {
|
||||
permission: TPermission;
|
||||
group: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UserAccessPermission<TPermission extends string> {
|
||||
permission: TPermission;
|
||||
user: {
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SimpleMutation<TPermission extends string> {
|
||||
mutate: (
|
||||
props: { entityId: string; permissions: { principalId: string; permission: TPermission }[] },
|
||||
options: { onSuccess: () => void },
|
||||
) => void;
|
||||
isPending: boolean;
|
||||
}
|
||||
|
||||
export interface AccessQueryData<TPermission extends string> {
|
||||
inherited: GroupAccessPermission<GroupPermissionKey>[];
|
||||
groups: GroupAccessPermission<TPermission>[];
|
||||
users: UserAccessPermission<TPermission>[];
|
||||
}
|
||||
|
||||
interface Props<TPermission extends string> {
|
||||
permission: {
|
||||
items: readonly TPermission[];
|
||||
default: TPermission;
|
||||
icons: Record<TPermission, TablerIcon>;
|
||||
groupPermissionMapping: Record<TPermission, GroupPermissionKey>;
|
||||
fullAccessGroupPermission: GroupPermissionKey;
|
||||
};
|
||||
|
||||
query: {
|
||||
data: AccessQueryData<TPermission>;
|
||||
invalidate: () => Promise<void>;
|
||||
};
|
||||
groupsMutation: SimpleMutation<TPermission>;
|
||||
usersMutation: SimpleMutation<TPermission>;
|
||||
entity: {
|
||||
id: string;
|
||||
ownerId: string | null;
|
||||
owner: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
image: string | null;
|
||||
} | null;
|
||||
};
|
||||
translate: (key: TPermission) => string;
|
||||
}
|
||||
|
||||
export const AccessSettings = <TPermission extends string>({
|
||||
permission,
|
||||
query,
|
||||
groupsMutation,
|
||||
usersMutation,
|
||||
entity,
|
||||
translate,
|
||||
}: Props<TPermission>) => {
|
||||
const [counts, setCounts] = useState({
|
||||
user: query.data.users.length + (entity.owner ? 1 : 0),
|
||||
group: query.data.groups.length,
|
||||
});
|
||||
|
||||
const handleGroupSubmit = (values: AccessFormType<TPermission>) => {
|
||||
groupsMutation.mutate(
|
||||
{
|
||||
entityId: entity.id,
|
||||
permissions: values.items,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
void query.invalidate();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleUserSubmit = (values: AccessFormType<TPermission>) => {
|
||||
usersMutation.mutate(
|
||||
{
|
||||
entityId: entity.id,
|
||||
permissions: values.items,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
void query.invalidate();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccessProvider<TPermission>
|
||||
defaultPermission={permission.default}
|
||||
icons={permission.icons}
|
||||
permissions={permission.items}
|
||||
translate={translate}
|
||||
>
|
||||
<Stack>
|
||||
<Tabs color="red" defaultValue="user">
|
||||
<Tabs.List grow>
|
||||
<TabItem value="user" count={counts.user} icon={IconUser} />
|
||||
<TabItem value="group" count={counts.group} icon={IconUsersGroup} />
|
||||
<TabItem value="inherited" count={query.data.inherited.length} icon={IconUserDown} />
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="user">
|
||||
<UsersAccessForm<TPermission>
|
||||
entity={entity}
|
||||
accessQueryData={query.data}
|
||||
handleCountChange={(callback) =>
|
||||
setCounts(({ user, ...others }) => ({
|
||||
user: callback(user),
|
||||
...others,
|
||||
}))
|
||||
}
|
||||
handleSubmit={handleUserSubmit}
|
||||
isPending={usersMutation.isPending}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="group">
|
||||
<GroupAccessForm<TPermission>
|
||||
accessQueryData={query.data}
|
||||
handleCountChange={(callback) =>
|
||||
setCounts(({ group, ...others }) => ({
|
||||
group: callback(group),
|
||||
...others,
|
||||
}))
|
||||
}
|
||||
handleSubmit={handleGroupSubmit}
|
||||
isPending={groupsMutation.isPending}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="inherited">
|
||||
<InheritAccessTable<TPermission>
|
||||
accessQueryData={query.data}
|
||||
fullAccessGroupPermission={permission.fullAccessGroupPermission}
|
||||
mapPermissions={permission.groupPermissionMapping}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
</AccessProvider>
|
||||
);
|
||||
};
|
||||
|
||||
interface TabItemProps {
|
||||
value: "user" | "group" | "inherited";
|
||||
count: number;
|
||||
icon: TablerIcon;
|
||||
}
|
||||
|
||||
const TabItem = ({ value, icon: Icon, count }: TabItemProps) => {
|
||||
const t = useScopedI18n("permission");
|
||||
|
||||
return (
|
||||
<Tabs.Tab value={value} leftSection={<Icon stroke={1.5} size={16} />}>
|
||||
<Group gap="sm">
|
||||
{t(`tab.${value}`)}
|
||||
<CountBadge count={count} />
|
||||
</Group>
|
||||
</Tabs.Tab>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user