feat: add app permissions management to role editing and enhance roles table with app data

This commit is contained in:
biersoeckli
2025-03-06 17:43:44 +00:00
parent 9ae6168de4
commit 79caa1ecb3
6 changed files with 129 additions and 19 deletions

View File

@@ -1,18 +1,14 @@
'use client'
import { Button } from "@/components/ui/button";
import { SimpleDataTable } from "@/components/custom/simple-data-table";
import { formatDateTime } from "@/frontend/utils/format.utils";
import { List } from "lucide-react";
import { BackupInfoModel } from "@/shared/model/backup-info.model";
import { BackupDetailDialog } from "./backup-detail-overlay";
export default function BackupsTable({ data }: { data: BackupInfoModel[] }) {
return <>
<SimpleDataTable columns={[
['projectId', 'Project ID', false],

View File

@@ -17,12 +17,14 @@ import {
TabsTrigger,
} from "@/components/ui/tabs"
import RolesTable from "./roles-table";
import appService from "@/server/services/app.service";
export default async function S3TargetsPage() {
await getAuthUserSession(); // todo only admins
const users = await userService.getAllUsers();
const roles = await roleService.getAll();
const allApps = await appService.getAll();
return (
<div className="flex-1 space-y-4 pt-6">
<PageTitle
@@ -41,7 +43,7 @@ export default async function S3TargetsPage() {
<UsersTable users={users} roles={roles} />
</TabsContent>
<TabsContent value="roles">
<RolesTable roles={roles} />
<RolesTable apps={allApps} roles={roles} />
</TabsContent>
</Tabs>
</div>

View File

@@ -22,15 +22,23 @@ import { ScrollArea } from "@/components/ui/scroll-area"
import { saveRole } from "./actions"
import { RoleExtended } from "@/shared/model/role-extended.model.ts"
import { RoleEditModel, roleEditZodModel } from "@/shared/model/role-edit.model"
import { App } from "@prisma/client"
import { Checkbox } from "@/components/ui/checkbox"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
export default function RoleEditOverlay({ children, role }: {
export default function RoleEditOverlay({ children, role, apps }: {
children: React.ReactNode;
role?: RoleExtended;
apps: App[]
}) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [appPermissions, setAppPermissions] = useState<{
appId: string;
read: boolean;
readwrite: boolean;
}[]>([]);
const form = useForm<RoleEditModel>({
resolver: zodResolver(roleEditZodModel),
defaultValues: role
@@ -40,7 +48,14 @@ export default function RoleEditOverlay({ children, role }: {
payload: RoleEditModel) =>
saveRole(state, {
...payload,
id: role?.id
id: role?.id,
roleAppPermissions: appPermissions.flatMap(perm => {
if (!perm.read && !perm.readwrite) return [];
return [{
appId: perm.appId,
permission: perm.readwrite ? 'READWRITE' : 'READ'
}];
})
}), FormUtils.getInitialFormState<typeof roleEditZodModel>());
useEffect(() => {
@@ -55,21 +70,70 @@ export default function RoleEditOverlay({ children, role }: {
useEffect(() => {
if (role) {
form.reset(role);
// Initialize app permissions based on role data
const initialPermissions = apps.map(app => {
const existingPermission = role.roleAppPermissions?.find(p => p.appId === app.id);
return {
appId: app.id,
read: !!existingPermission && existingPermission.permission === 'READ',
readwrite: !!existingPermission && existingPermission.permission === 'READWRITE'
};
});
setAppPermissions(initialPermissions);
} else {
// Initialize with all apps having no permissions
const initialPermissions = apps.map(app => ({
appId: app.id,
read: false,
readwrite: false
}));
setAppPermissions(initialPermissions);
}
}, [role]);
}, [role, apps]);
const handleReadChange = (appId: string, checked: boolean) => {
setAppPermissions(prev => prev.map(perm => {
if (perm.appId === appId) {
// If read is being turned off, also turn off readwrite
if (!checked) {
return { ...perm, read: false, readwrite: false };
}
// If read is being turned on, just update read
return { ...perm, read: checked };
}
return perm;
}));
};
const handleReadWriteChange = (appId: string, checked: boolean) => {
setAppPermissions(prev => prev.map(perm => {
if (perm.appId === appId) {
// If readwrite is being turned on, also turn on read
if (checked) {
return { ...perm, read: true, readwrite: true };
}
// If readwrite is being turned off, just update readwrite
return { ...perm, readwrite: false };
}
return perm;
}));
};
return (
<>
<div onClick={() => setIsOpen(true)}>
{children}
</div>
<Dialog open={!!isOpen} onOpenChange={(isOpened) => setIsOpen(false)}>
<DialogContent className="sm:max-w-[425px]">
<Dialog open={!!isOpen} onOpenChange={(isOpened) => setIsOpen(isOpened)}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{role?.id ? 'Edit' : 'Create'} Role</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[70vh]">
<div className="px-2">
<div className="px-3">
<Form {...form}>
<form action={(e) => form.handleSubmit((data) => {
return formAction(data);
@@ -89,6 +153,43 @@ export default function RoleEditOverlay({ children, role }: {
)}
/>
<div className="pt-3">
<h3 className="text-sm font-medium mb-2">App Permissions</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>App</TableHead>
<TableHead>Read</TableHead>
<TableHead>ReadWrite</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apps.map((app) => {
const permission = appPermissions.find(p => p.appId === app.id);
return (
<TableRow key={app.id}>
<TableCell>{app.name}</TableCell>
<TableCell>
<Checkbox
id={`read-${app.id}`}
disabled={permission?.readwrite}
checked={permission?.read || false}
onCheckedChange={(checked) => handleReadChange(app.id, !!checked)}
/>
</TableCell>
<TableCell>
<Checkbox
id={`readwrite-${app.id}`}
checked={permission?.readwrite || false}
onCheckedChange={(checked) => handleReadWriteChange(app.id, !!checked)}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
<p className="text-red-500">{state.message}</p>
<SubmitButton>Save</SubmitButton>
@@ -101,7 +202,4 @@ export default function RoleEditOverlay({ children, role }: {
</Dialog>
</>
)
}

View File

@@ -10,9 +10,11 @@ import { formatDateTime } from "@/frontend/utils/format.utils";
import { deleteRole } from "./actions";
import { RoleExtended } from "@/shared/model/role-extended.model.ts";
import RoleEditOverlay from "./role-edit-overlay";
import { App } from "@prisma/client";
export default function RolesTable({ roles }: {
export default function RolesTable({ roles, apps }: {
roles: RoleExtended[];
apps: App[];
}) {
const { openConfirmDialog: openDialog } = useConfirmDialog();
@@ -42,7 +44,7 @@ export default function RolesTable({ roles }: {
<>
<div className="flex">
<div className="flex-1"></div>
<RoleEditOverlay role={item} >
<RoleEditOverlay apps={apps} role={item} >
<Button variant="ghost"><EditIcon /></Button>
</RoleEditOverlay>
<Button variant="ghost" onClick={() => asyncDeleteItem(item.id)}>
@@ -51,7 +53,7 @@ export default function RolesTable({ roles }: {
</div>
</>}
/>
<RoleEditOverlay >
<RoleEditOverlay apps={apps} >
<Button variant="secondary"><Plus /> Create Role</Button>
</RoleEditOverlay>
</>;

View File

@@ -468,6 +468,14 @@ class AppService {
revalidateTag(Tags.apps(existingItem.app.projectId));
}
}
async getAll() {
return await dataAccess.client.app.findMany({
orderBy: {
name: 'asc'
}
});
}
}
const appService = new AppService();

View File

@@ -4,6 +4,10 @@ import { z } from "zod";
export const roleEditZodModel = z.object({
id: z.string().trim().optional(),
name: z.string().trim().min(1),
roleAppPermissions: z.array(z.object({
appId: z.string(),
permission: z.string(),
})).optional(),
})
export type RoleEditModel = z.infer<typeof roleEditZodModel>;