From c6925bbbca2806dd3c7cf0097a036f0bfa76c1b6 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Thu, 10 Apr 2025 02:37:54 +0000 Subject: [PATCH] Add custom role management functionality. This includes creating, reading, updating, and deleting custom roles. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/a2c93ffa-6494-4413-93ca-36f98dc098cd.jpg --- client/src/pages/roles.tsx | 1075 +++++++++++++----------------------- server/routes.ts | 138 ++++- 2 files changed, 522 insertions(+), 691 deletions(-) diff --git a/client/src/pages/roles.tsx b/client/src/pages/roles.tsx index e0dd2b1..420772f 100644 --- a/client/src/pages/roles.tsx +++ b/client/src/pages/roles.tsx @@ -1,30 +1,23 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; -import { MainLayout } from "@/components/layouts/main-layout"; -import { useAuth } from "@/hooks/use-auth"; -import { apiRequest, queryClient } from "@/lib/queryClient"; +import { queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; +import { useAuth } from "@/hooks/use-auth"; +import { apiRequest } from "@/lib/queryClient"; import { z } from "zod"; -import { CustomRole, InsertCustomRole, SystemRole, systemRoles } from "@shared/schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { SystemRole, systemRoles, CustomRole } from "@shared/schema"; + +// UI Components import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Dialog, DialogContent, @@ -33,6 +26,7 @@ import { DialogHeader, DialogTitle, DialogTrigger, + DialogClose, } from "@/components/ui/dialog"; import { AlertDialog, @@ -46,231 +40,129 @@ import { AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Button } from "@/components/ui/button"; + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Checkbox } from "@/components/ui/checkbox"; import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { Loader2, MoreVertical, ShieldAlert, ShieldCheck, ShieldX, Lock, Key, FileText, Edit, Trash, Plus, Shield } from "lucide-react"; -import { formatDistanceToNow } from "date-fns"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; -// Available permissions for custom roles +import { Loader2, PlusCircle, Edit, Trash2, ShieldAlert, ShieldCheck } from "lucide-react"; + +// Available permissions const availablePermissions = [ - { id: "domain.read", label: "View Domains" }, - { id: "domain.create", label: "Create Domains" }, - { id: "domain.update", label: "Edit Domains" }, - { id: "domain.delete", label: "Delete Domains" }, - { id: "dnsrecord.read", label: "View DNS Records" }, - { id: "dnsrecord.create", label: "Create DNS Records" }, - { id: "dnsrecord.update", label: "Edit DNS Records" }, - { id: "dnsrecord.delete", label: "Delete DNS Records" }, - { id: "user.read", label: "View Users" }, - { id: "user.create", label: "Create Users" }, - { id: "user.update", label: "Edit Users" }, - { id: "user.delete", label: "Delete Users" }, - { id: "provider.read", label: "View DNS Providers" }, - { id: "provider.create", label: "Add DNS Providers" }, - { id: "provider.update", label: "Edit DNS Providers" }, - { id: "provider.delete", label: "Remove DNS Providers" }, - { id: "webhook.read", label: "View Webhooks" }, - { id: "webhook.create", label: "Create Webhooks" }, - { id: "webhook.update", label: "Edit Webhooks" }, - { id: "webhook.delete", label: "Delete Webhooks" }, - { id: "apitoken.read", label: "View API Tokens" }, - { id: "apitoken.create", label: "Create API Tokens" }, - { id: "apitoken.update", label: "Edit API Tokens" }, - { id: "apitoken.delete", label: "Delete API Tokens" }, - { id: "metrics.read", label: "View Metrics" }, - { id: "history.read", label: "View DNS History" }, - { id: "organization.read", label: "View Organizations" }, - { id: "organization.create", label: "Create Organizations" }, - { id: "organization.update", label: "Edit Organizations" }, - { id: "organization.delete", label: "Delete Organizations" }, - { id: "group.read", label: "View Groups" }, - { id: "group.create", label: "Create Groups" }, - { id: "group.update", label: "Edit Groups" }, - { id: "group.delete", label: "Delete Groups" }, - { id: "role.read", label: "View Roles" }, - { id: "role.create", label: "Create Roles" }, - { id: "role.update", label: "Edit Roles" }, - { id: "role.delete", label: "Delete Roles" }, + { id: "domains.view", label: "View Domains" }, + { id: "domains.create", label: "Create Domains" }, + { id: "domains.edit", label: "Edit Domains" }, + { id: "domains.delete", label: "Delete Domains" }, + { id: "records.view", label: "View DNS Records" }, + { id: "records.create", label: "Create DNS Records" }, + { id: "records.edit", label: "Edit DNS Records" }, + { id: "records.delete", label: "Delete DNS Records" }, + { id: "providers.view", label: "View Providers" }, + { id: "providers.create", label: "Create Providers" }, + { id: "providers.edit", label: "Edit Providers" }, + { id: "providers.delete", label: "Delete Providers" }, + { id: "webhooks.view", label: "View Webhooks" }, + { id: "webhooks.create", label: "Create Webhooks" }, + { id: "webhooks.edit", label: "Edit Webhooks" }, + { id: "webhooks.delete", label: "Delete Webhooks" }, + { id: "api-tokens.view", label: "View API Tokens" }, + { id: "api-tokens.create", label: "Create API Tokens" }, + { id: "api-tokens.edit", label: "Edit API Tokens" }, + { id: "api-tokens.delete", label: "Delete API Tokens" }, + { id: "organizations.view", label: "View Organizations" }, + { id: "organizations.create", label: "Create Organizations" }, + { id: "organizations.edit", label: "Edit Organizations" }, + { id: "organizations.delete", label: "Delete Organizations" }, + { id: "users.view", label: "View Users" }, + { id: "users.create", label: "Create Users" }, + { id: "users.edit", label: "Edit Users" }, + { id: "users.delete", label: "Delete Users" }, + { id: "roles.view", label: "View Roles" }, + { id: "roles.create", label: "Create Roles" }, + { id: "roles.edit", label: "Edit Roles" }, + { id: "roles.delete", label: "Delete Roles" }, + { id: "metrics.view", label: "View Metrics" }, + { id: "history.view", label: "View History" }, ]; -// Group permissions by category -const permissionCategories = [ - { - id: "domain", - name: "Domains", - permissions: availablePermissions.filter(p => p.id.startsWith("domain.")), - }, - { - id: "dns", - name: "DNS Records", - permissions: availablePermissions.filter(p => p.id.startsWith("dnsrecord.")), - }, - { - id: "user", - name: "Users", - permissions: availablePermissions.filter(p => p.id.startsWith("user.")), - }, - { - id: "provider", - name: "DNS Providers", - permissions: availablePermissions.filter(p => p.id.startsWith("provider.")), - }, - { - id: "webhook", - name: "Webhooks", - permissions: availablePermissions.filter(p => p.id.startsWith("webhook.")), - }, - { - id: "apitoken", - name: "API Tokens", - permissions: availablePermissions.filter(p => p.id.startsWith("apitoken.")), - }, - { - id: "organization", - name: "Organizations", - permissions: availablePermissions.filter(p => p.id.startsWith("organization.")), - }, - { - id: "group", - name: "Groups", - permissions: availablePermissions.filter(p => p.id.startsWith("group.")), - }, - { - id: "role", - name: "Roles", - permissions: availablePermissions.filter(p => p.id.startsWith("role.")), - }, - { - id: "other", - name: "Other", - permissions: availablePermissions.filter(p => - p.id.startsWith("metrics.") || p.id.startsWith("history.")), - }, -]; +// Default permissions for system roles +const systemRolePermissions: Record = { + admin: availablePermissions.map(p => p.id), + manager: [ + "domains.view", "domains.create", "domains.edit", "domains.delete", + "records.view", "records.create", "records.edit", "records.delete", + "webhooks.view", "webhooks.create", "webhooks.edit", "webhooks.delete", + "api-tokens.view", "api-tokens.create", "api-tokens.edit", "api-tokens.delete", + "metrics.view", "history.view", + ], + user: [ + "domains.view", + "records.view", "records.create", "records.edit", + "webhooks.view", + "api-tokens.view", + "metrics.view", "history.view", + ], + readonly: [ + "domains.view", + "records.view", + "webhooks.view", + "api-tokens.view", + "metrics.view", "history.view", + ], +}; -// Zod schema for role form +// Form validation schema const roleFormSchema = z.object({ - name: z.string().min(3, "Name must be at least 3 characters"), - description: z.string().optional(), + name: z.string().min(3, "Name must be at least 3 characters").max(50, "Name cannot exceed 50 characters"), + description: z.string().max(255, "Description cannot exceed 255 characters").optional(), permissions: z.array(z.string()).min(1, "Select at least one permission"), - isActive: z.boolean().default(true), }); type RoleFormValues = z.infer; -// Role card component for system roles +// System role card component const SystemRoleCard = ({ roleName }: { roleName: SystemRole }) => { - const roleInfo = { - admin: { - title: "Administrator", - description: "Full system access with all permissions", - icon: , - color: "bg-destructive/10 text-destructive border-destructive/20", - }, - manager: { - title: "Manager", - description: "Can manage domains and records, but not users or system settings", - icon: , - color: "bg-warning/10 text-warning border-warning/20", - }, - user: { - title: "User", - description: "Can manage assigned domains and records", - icon: , - color: "bg-primary/10 text-primary border-primary/20", - }, - readonly: { - title: "Read-only", - description: "View-only access to assigned domains and records", - icon: , - color: "bg-muted/10 text-muted-foreground border-muted/20", - }, - }[roleName]; - return ( - - -
-
- {roleInfo.icon} -
- {roleInfo.title} - System Role -
-
- - {roleName} - -
+ + + + + {roleName.charAt(0).toUpperCase() + roleName.slice(1)} + System + + + {roleName === "admin" && "Full access to all system features and settings"} + {roleName === "manager" && "Manage domains, records, and access to organizational resources"} + {roleName === "user" && "Standard user with ability to view and modify specific resources"} + {roleName === "readonly" && "View-only access to resources without modification permissions"} + -

{roleInfo.description}

- -
-

Permissions

- {roleName === "admin" ? ( -
- - All permissions -
- ) : ( -
- {roleName === "readonly" ? ( - - Read-only access - - ) : roleName === "manager" ? ( - <> - - Domain management - - - DNS record management - - - Webhook management - - - ) : ( - <> - - Domain access - - - DNS record management - - - )} -
- )} +
+ {systemRolePermissions[roleName].map((permission) => ( + + {availablePermissions.find(p => p.id === permission)?.label || permission} + + ))}
@@ -282,36 +174,14 @@ export default function RolesPage() { const { user } = useAuth(); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [selectedRole, setSelectedRole] = useState(null); - const [activeTab, setActiveTab] = useState("system-roles"); + const [currentRole, setCurrentRole] = useState(null); + + const isAdmin = user?.role === "admin"; // Fetch custom roles - const { data: customRoles = [], isLoading, error } = useQuery({ + const { data: customRoles, isLoading } = useQuery({ queryKey: ["/api/roles/custom"], - enabled: !!user && user.role === "admin", - }); - - // Create role form - const createRoleForm = useForm({ - resolver: zodResolver(roleFormSchema), - defaultValues: { - name: "", - description: "", - permissions: [], - isActive: true, - }, - }); - - // Edit role form - const editRoleForm = useForm({ - resolver: zodResolver(roleFormSchema), - defaultValues: { - name: "", - description: "", - permissions: [], - isActive: true, - }, + enabled: isAdmin, }); // Create role mutation @@ -324,13 +194,12 @@ export default function RolesPage() { return await res.json(); }, onSuccess: () => { - setIsCreateDialogOpen(false); - createRoleForm.reset(); queryClient.invalidateQueries({ queryKey: ["/api/roles/custom"] }); toast({ title: "Role created", - description: "The custom role has been created successfully.", + description: "The role has been created successfully.", }); + setIsCreateDialogOpen(false); }, onError: (error: Error) => { toast({ @@ -343,19 +212,18 @@ export default function RolesPage() { // Update role mutation const updateRoleMutation = useMutation({ - mutationFn: async ({ id, data }: { id: string; data: Partial }) => { - const res = await apiRequest("PUT", `/api/roles/custom/${id}`, data); + mutationFn: async (data: { id: string; values: Partial }) => { + const res = await apiRequest("PUT", `/api/roles/custom/${data.id}`, data.values); return await res.json(); }, onSuccess: () => { - setIsEditDialogOpen(false); - editRoleForm.reset(); - setSelectedRole(null); queryClient.invalidateQueries({ queryKey: ["/api/roles/custom"] }); toast({ title: "Role updated", - description: "The custom role has been updated successfully.", + description: "The role has been updated successfully.", }); + setIsEditDialogOpen(false); + setCurrentRole(null); }, onError: (error: Error) => { toast({ @@ -372,12 +240,10 @@ export default function RolesPage() { await apiRequest("DELETE", `/api/roles/custom/${id}`); }, onSuccess: () => { - setIsDeleteDialogOpen(false); - setSelectedRole(null); queryClient.invalidateQueries({ queryKey: ["/api/roles/custom"] }); toast({ title: "Role deleted", - description: "The custom role has been deleted successfully.", + description: "The role has been deleted successfully.", }); }, onError: (error: Error) => { @@ -389,331 +255,231 @@ export default function RolesPage() { }, }); - // Handle role edit + // Setup forms + const createForm = useForm({ + resolver: zodResolver(roleFormSchema), + defaultValues: { + name: "", + description: "", + permissions: [], + }, + }); + + const editForm = useForm({ + resolver: zodResolver(roleFormSchema), + defaultValues: { + name: "", + description: "", + permissions: [], + }, + }); + const handleEditRole = (role: CustomRole) => { - setSelectedRole(role); - editRoleForm.reset({ + setCurrentRole(role); + editForm.reset({ name: role.name, description: role.description || "", permissions: role.permissions, - isActive: role.isActive, }); setIsEditDialogOpen(true); }; - // Handle role deletion const handleDeleteRole = (role: CustomRole) => { - setSelectedRole(role); - setIsDeleteDialogOpen(true); + if (confirm(`Are you sure you want to delete the role "${role.name}"?`)) { + deleteRoleMutation.mutate(role.id); + } }; - // Submit handlers const onCreateSubmit = (data: RoleFormValues) => { createRoleMutation.mutate(data); }; const onEditSubmit = (data: RoleFormValues) => { - if (selectedRole) { - updateRoleMutation.mutate({ id: selectedRole.id, data }); + if (currentRole) { + updateRoleMutation.mutate({ + id: currentRole.id, + values: data, + }); } }; - const confirmDeleteRole = () => { - if (selectedRole) { - deleteRoleMutation.mutate(selectedRole.id); - } - }; - - // Check if current user is admin - if (user?.role !== "admin") { + if (!isAdmin) { return ( - - - - -

Access Restricted

-

- You don't have permission to access this page. Only administrators can manage roles and permissions. -

-
-
-
+
+

Role Management

+
+

+ You don't have permission to access this page. Admin privileges required. +

+
+
); } return ( - -
-
-

Role Management

-

- Define user roles and their permissions -

-
+
+
+

Role Management

- - - System Roles - Custom Roles - - - {/* System Roles Tab */} - -
+
+
+

System Roles

+
{systemRoles.map((role) => ( - + ))}
- +
- {/* Custom Roles Tab */} - +
+

Custom Roles

{isLoading ? ( -
+
- ) : customRoles.length === 0 ? ( + ) : !customRoles || customRoles.length === 0 ? ( - -
- -
-

No Custom Roles

-

- Create custom roles to define specific permissions for users in your organization. -

- + + No custom roles available. Create one using the "Create Role" button.
) : (
- {customRoles.map((role) => ( - - -
-
-
- -
-
- {role.name} - - {role.description || "Custom role"} - -
+ {customRoles.map((role: CustomRole) => ( + + +
+
+ + + {role.name} + Custom + + {role.description && ( + {role.description} + )} +
+
+ +
- - - - - - handleEditRole(role)}> - - Edit - - handleDeleteRole(role)} - > - - Delete - - -
-
-
- Status - - {role.isActive ? "Active" : "Inactive"} +
+ {role.permissions.map((permission) => ( + + {availablePermissions.find(p => p.id === permission)?.label || permission} -
-
- Created - - {formatDistanceToNow(new Date(role.createdAt), { addSuffix: true })} - -
-
- -
-

Permissions ({role.permissions.length})

-
- {role.permissions.slice(0, 5).map((permission) => ( - - {permission} - - ))} - {role.permissions.length > 5 && ( - - +{role.permissions.length - 5} more - - )} -
+ ))}
))}
)} - - +
+
{/* Create Role Dialog */} - + - Create Custom Role + Create New Role - Define a new role with custom permissions. + Define a new custom role with specific permissions. - -
- -
- ( - - Role Name - - - - - - )} - /> - - ( - - Description - -