import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { MainLayout } from "@/components/layouts/main-layout"; import { User, InsertUser, systemRoles, UserRole } from "@shared/schema"; 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Pagination } from "@/components/shared/pagination"; import { Loader2, MoreVertical, UserPlus, Lock, Shield, UserCog } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { useOrganization } from "@/context/organization-context"; // User form schema const userFormSchema = z.object({ username: z.string().min(3, "Username must be at least 3 characters"), email: z.string().email("Please enter a valid email"), password: z.string().min(8, "Password must be at least 8 characters"), fullName: z.string().optional(), role: z.enum(systemRoles as unknown as [string, ...string[]]), organizationId: z.number().optional(), }); export default function UsersRolesPage() { const { toast } = useToast(); const { user } = useAuth(); const { currentOrganization } = useOrganization(); const [isAddUserDialogOpen, setIsAddUserDialogOpen] = useState(false); const [isEditRoleDialogOpen, setIsEditRoleDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [currentPage, setCurrentPage] = useState(1); const pageSize = 10; // Fetch users const { data: users = [], isLoading } = useQuery({ queryKey: ["/api/users"], enabled: !!user && user.role === "admin", }); // Fetch organizations for select const { data: organizations = [] } = useQuery({ queryKey: ["/api/organizations"], }); // Form for adding a user const addUserForm = useForm>({ resolver: zodResolver(userFormSchema), defaultValues: { username: "", email: "", password: "", fullName: "", role: "user", organizationId: currentOrganization?.id, }, }); // Form for editing a user's role const editRoleForm = useForm<{ role: UserRole }>({ resolver: zodResolver(z.object({ role: z.enum(systemRoles as unknown as [string, ...string[]]), })), defaultValues: { role: "user", }, }); // Add user mutation const addUserMutation = useMutation({ mutationFn: async (data: z.infer) => { const res = await apiRequest("POST", "/api/register", data); return await res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/users"] }); setIsAddUserDialogOpen(false); addUserForm.reset(); toast({ title: "User added", description: "The user has been successfully added.", }); }, onError: (error) => { toast({ title: "Failed to add user", description: error.message, variant: "destructive", }); }, }); // Update user role mutation const updateRoleMutation = useMutation({ mutationFn: async ({ userId, role }: { userId: number, role: UserRole }) => { const res = await apiRequest("PUT", `/api/users/${userId}`, { role }); return await res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/users"] }); setIsEditRoleDialogOpen(false); setSelectedUser(null); toast({ title: "Role updated", description: "The user's role has been successfully updated.", }); }, onError: (error) => { toast({ title: "Failed to update role", description: error.message, variant: "destructive", }); }, }); // Delete user mutation const deleteUserMutation = useMutation({ mutationFn: async (userId: number) => { await apiRequest("DELETE", `/api/users/${userId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/users"] }); setIsDeleteDialogOpen(false); setSelectedUser(null); toast({ title: "User deleted", description: "The user has been successfully deleted.", }); }, onError: (error) => { toast({ title: "Failed to delete user", description: error.message, variant: "destructive", }); }, }); // Form submission handler for adding user const onAddUserSubmit = (data: z.infer) => { addUserMutation.mutate(data); }; // Form submission handler for editing role const onEditRoleSubmit = (data: { role: UserRole }) => { if (selectedUser) { updateRoleMutation.mutate({ userId: selectedUser.id, role: data.role }); } }; // Handle role editing const handleEditRole = (user: User) => { setSelectedUser(user); editRoleForm.reset({ role: user.role as UserRole }); setIsEditRoleDialogOpen(true); }; // Handle user deletion const handleDeleteUser = (user: User) => { setSelectedUser(user); setIsDeleteDialogOpen(true); }; // Confirm deletion const confirmDeleteUser = () => { if (selectedUser) { deleteUserMutation.mutate(selectedUser.id); } }; // Get role badge const getRoleBadge = (role: string) => { switch (role) { case "admin": return ( Admin ); case "manager": return ( Manager ); case "user": return ( User ); case "readonly": return ( Read-only ); default: return ( {role} ); } }; // Calculate pagination const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedUsers = users.slice(startIndex, endIndex); const totalPages = Math.ceil(users.length / pageSize); // 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 users and roles.

); } return (

User Management

Add, edit, and manage user accounts and their permissions.

{/* Role descriptions card */} Role Permissions Understanding the different access levels in DynamiDNS

Administrator

Full access to all features, including user management, organization settings, and system configuration.

Manager

Can manage domains, DNS records, and API tokens, but cannot modify system settings or manage users.

User

Can view domains and manage DNS records, but cannot add domains or manage API tokens.

Read-only

Can only view domains and DNS records without making any changes.

{/* Users Table */} Users Manage user accounts and their roles {isLoading ? (
) : users.length === 0 ? (

No users found

Get started by adding your first user.

) : ( <>
Username Email Name Role Organization Created Actions {paginatedUsers.map((user) => ( {user.username} {user.email} {user.fullName || "-"} {getRoleBadge(user.role)} {user.organizationId ? organizations.find(org => org.id === user.organizationId)?.name || `Org ID: ${user.organizationId}` : "-"} {formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })} handleEditRole(user)} > Change Role handleDeleteUser(user)} className="text-destructive focus:text-destructive" disabled={user.id === user.id} // Can't delete yourself > Delete ))}
{users.length > pageSize && (
)} )}
{/* Add User Dialog */} Add User Create a new user account with specified permissions.
( Username )} /> ( Email )} /> ( Password )} /> ( Full Name (Optional) )} /> ( Role )} /> ( Organization )} />
{/* Edit Role Dialog */} Change User Role Update the role for user {selectedUser?.username}.
( New Role )} />
{/* Delete Confirmation Dialog */} Delete User This will permanently delete the user {selectedUser?.username}. All data associated with this user will be lost. This action cannot be undone. Cancel {deleteUserMutation.isPending ? ( <> Deleting... ) : ( "Delete" )}
); }