From 35d2af8410e7fe44709be247e9eca561862bd6a4 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Thu, 10 Apr 2025 02:34:51 +0000 Subject: [PATCH] Add role management page 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/d62e0c80-a2f3-41da-afc0-1d9e4fd98854.jpg --- client/src/App.tsx | 2 + client/src/pages/roles.tsx | 905 +++++++++++++++++++++++++++++++++++++ 2 files changed, 907 insertions(+) create mode 100644 client/src/pages/roles.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index bb13f0a..89fbe2e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -11,6 +11,7 @@ import MetricsPage from "@/pages/metrics"; import HistoryPage from "@/pages/history"; import ApiTokensPage from "@/pages/api-tokens"; import UsersRolesPage from "@/pages/users-roles"; +import RolesPage from "@/pages/roles"; import ProvidersPage from "@/pages/providers"; import SettingsPage from "@/pages/settings"; import OrganizationsPage from "@/pages/organizations"; @@ -33,6 +34,7 @@ function Router() { + diff --git a/client/src/pages/roles.tsx b/client/src/pages/roles.tsx new file mode 100644 index 0000000..e0dd2b1 --- /dev/null +++ b/client/src/pages/roles.tsx @@ -0,0 +1,905 @@ +import { useState, useEffect } 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 { useToast } from "@/hooks/use-toast"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { CustomRole, InsertCustomRole, SystemRole, systemRoles } from "@shared/schema"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + 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"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Textarea } from "@/components/ui/textarea"; +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"; + +// Available permissions for custom roles +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" }, +]; + +// 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.")), + }, +]; + +// Zod schema for role form +const roleFormSchema = z.object({ + name: z.string().min(3, "Name must be at least 3 characters"), + description: z.string().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 +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} + +
+
+ +

{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 + + + )} +
+ )} +
+
+
+ ); +}; + +export default function RolesPage() { + const { toast } = useToast(); + 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"); + + // Fetch custom roles + const { data: customRoles = [], isLoading, error } = 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, + }, + }); + + // Create role mutation + const createRoleMutation = useMutation({ + mutationFn: async (data: RoleFormValues) => { + const res = await apiRequest("POST", "/api/roles/custom", { + ...data, + createdBy: user?.id, + }); + 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.", + }); + }, + onError: (error: Error) => { + toast({ + title: "Error creating role", + description: error.message, + variant: "destructive", + }); + }, + }); + + // Update role mutation + const updateRoleMutation = useMutation({ + mutationFn: async ({ id, data }: { id: string; data: Partial }) => { + const res = await apiRequest("PUT", `/api/roles/custom/${id}`, data); + 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.", + }); + }, + onError: (error: Error) => { + toast({ + title: "Error updating role", + description: error.message, + variant: "destructive", + }); + }, + }); + + // Delete role mutation + const deleteRoleMutation = useMutation({ + mutationFn: async (id: string) => { + 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.", + }); + }, + onError: (error: Error) => { + toast({ + title: "Error deleting role", + description: error.message, + variant: "destructive", + }); + }, + }); + + // Handle role edit + const handleEditRole = (role: CustomRole) => { + setSelectedRole(role); + editRoleForm.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); + }; + + // Submit handlers + const onCreateSubmit = (data: RoleFormValues) => { + createRoleMutation.mutate(data); + }; + + const onEditSubmit = (data: RoleFormValues) => { + if (selectedRole) { + updateRoleMutation.mutate({ id: selectedRole.id, data }); + } + }; + + const confirmDeleteRole = () => { + if (selectedRole) { + deleteRoleMutation.mutate(selectedRole.id); + } + }; + + // Check if current user is admin + if (user?.role !== "admin") { + return ( + + + + +

Access Restricted

+

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

+
+
+
+ ); + } + + return ( + +
+
+

Role Management

+

+ Define user roles and their permissions +

+
+ +
+ + + + System Roles + Custom Roles + + + {/* System Roles Tab */} + +
+ {systemRoles.map((role) => ( + + ))} +
+
+ + {/* Custom Roles Tab */} + + {isLoading ? ( +
+ +
+ ) : customRoles.length === 0 ? ( + + +
+ +
+

No Custom Roles

+

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

+ +
+
+ ) : ( +
+ {customRoles.map((role) => ( + + +
+
+
+ +
+
+ {role.name} + + {role.description || "Custom role"} + +
+
+ + + + + + handleEditRole(role)}> + + Edit + + handleDeleteRole(role)} + > + + Delete + + + +
+
+ +
+
+ Status + + {role.isActive ? "Active" : "Inactive"} + +
+
+ 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 + + Define a new role with custom permissions. + + + +
+ +
+ ( + + Role Name + + + + + + )} + /> + + ( + + Description + +