Add ability for administrators to manage user roles and permissions

Introduce a new user management interface for viewing and updating user roles, including API endpoints and backend storage logic for role management.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 99f0fce6-9386-489a-9632-1d81223cab44
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/99f0fce6-9386-489a-9632-1d81223cab44/TRdpk3a
This commit is contained in:
marco370 2025-10-11 15:16:47 +00:00
parent a9486f684c
commit 4443773040
6 changed files with 293 additions and 1 deletions

View File

@ -19,7 +19,7 @@ localPort = 33035
externalPort = 3001
[[ports]]
localPort = 33349
localPort = 38973
externalPort = 3002
[[ports]]

View File

@ -15,6 +15,7 @@ import Sites from "@/pages/sites";
import Shifts from "@/pages/shifts";
import Reports from "@/pages/reports";
import Notifications from "@/pages/notifications";
import Users from "@/pages/users";
function Router() {
const { isAuthenticated, isLoading } = useAuth();
@ -31,6 +32,7 @@ function Router() {
<Route path="/shifts" component={Shifts} />
<Route path="/reports" component={Reports} />
<Route path="/notifications" component={Notifications} />
<Route path="/users" component={Users} />
</>
)}
<Route component={NotFound} />

View File

@ -7,6 +7,7 @@ import {
Bell,
Settings,
LogOut,
UserCog,
} from "lucide-react";
import { Link, useLocation } from "wouter";
import {
@ -63,6 +64,12 @@ const menuItems = [
icon: Bell,
roles: ["admin", "coordinator", "guard"],
},
{
title: "Utenti",
url: "/users",
icon: UserCog,
roles: ["admin"],
},
];
export function AppSidebar() {

239
client/src/pages/users.tsx Normal file
View File

@ -0,0 +1,239 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient";
import { useAuth } from "@/hooks/useAuth";
import { type User } from "@shared/schema";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/hooks/use-toast";
import { Shield, UserCog, Users as UsersIcon, UserCheck } from "lucide-react";
const roleIcons = {
admin: Shield,
coordinator: UserCog,
guard: UsersIcon,
client: UserCheck,
};
const roleLabels = {
admin: "Admin",
coordinator: "Coordinatore",
guard: "Guardia",
client: "Cliente",
};
const roleColors = {
admin: "bg-red-500/10 text-red-500 border-red-500/20",
coordinator: "bg-blue-500/10 text-blue-500 border-blue-500/20",
guard: "bg-green-500/10 text-green-500 border-green-500/20",
client: "bg-orange-500/10 text-orange-500 border-orange-500/20",
};
export default function Users() {
const { user: currentUser } = useAuth();
const { toast } = useToast();
const { data: users, isLoading } = useQuery<User[]>({
queryKey: ["/api/users"],
});
const updateRoleMutation = useMutation({
mutationFn: async ({ userId, role }: { userId: string; role: string }) => {
return apiRequest("PATCH", `/api/users/${userId}`, { role });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/users"] });
queryClient.invalidateQueries({ queryKey: ["/api/auth/user"] });
toast({
title: "Ruolo aggiornato",
description: "Il ruolo dell'utente è stato modificato con successo.",
});
},
onError: () => {
toast({
title: "Errore",
description: "Impossibile aggiornare il ruolo dell'utente.",
variant: "destructive",
});
},
});
if (isLoading) {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Gestione Utenti</h1>
<p className="text-muted-foreground">
Gestisci utenti e permessi del sistema
</p>
</div>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-3 w-32" />
</div>
<Skeleton className="h-9 w-32" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold" data-testid="text-page-title">
Gestione Utenti
</h1>
<p className="text-muted-foreground">
Gestisci utenti e permessi del sistema VigilanzaTurni
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Utenti Registrati</CardTitle>
<CardDescription>
{users?.length || 0} utenti totali nel sistema
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Utente</TableHead>
<TableHead>Email</TableHead>
<TableHead>Ruolo Attuale</TableHead>
<TableHead>Modifica Ruolo</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users?.map((user) => {
const RoleIcon = roleIcons[user.role];
const isCurrentUser = user.id === currentUser?.id;
return (
<TableRow key={user.id} data-testid={`row-user-${user.id}`}>
<TableCell>
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={user.profileImageUrl || undefined} />
<AvatarFallback>
{user.firstName?.[0]}
{user.lastName?.[0]}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium" data-testid={`text-username-${user.id}`}>
{user.firstName} {user.lastName}
{isCurrentUser && (
<span className="ml-2 text-xs text-muted-foreground">
(Tu)
</span>
)}
</p>
<p className="text-sm text-muted-foreground">
ID: {user.id}
</p>
</div>
</div>
</TableCell>
<TableCell data-testid={`text-email-${user.id}`}>
{user.email}
</TableCell>
<TableCell>
<Badge
variant="outline"
className={roleColors[user.role]}
data-testid={`badge-role-${user.id}`}
>
<RoleIcon className="h-3 w-3 mr-1" />
{roleLabels[user.role]}
</Badge>
</TableCell>
<TableCell>
<Select
value={user.role}
onValueChange={(role) =>
updateRoleMutation.mutate({ userId: user.id, role })
}
disabled={isCurrentUser || updateRoleMutation.isPending}
data-testid={`select-role-${user.id}`}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin" data-testid={`option-admin-${user.id}`}>
<div className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Admin
</div>
</SelectItem>
<SelectItem value="coordinator" data-testid={`option-coordinator-${user.id}`}>
<div className="flex items-center gap-2">
<UserCog className="h-4 w-4" />
Coordinatore
</div>
</SelectItem>
<SelectItem value="guard" data-testid={`option-guard-${user.id}`}>
<div className="flex items-center gap-2">
<UsersIcon className="h-4 w-4" />
Guardia
</div>
</SelectItem>
<SelectItem value="client" data-testid={`option-client-${user.id}`}>
<div className="flex items-center gap-2">
<UserCheck className="h-4 w-4" />
Cliente
</div>
</SelectItem>
</SelectContent>
</Select>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{users?.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Nessun utente registrato nel sistema
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -23,6 +23,35 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// ============= USER MANAGEMENT ROUTES =============
app.get("/api/users", isAuthenticated, async (req, res) => {
try {
const allUsers = await storage.getAllUsers();
res.json(allUsers);
} catch (error) {
console.error("Error fetching users:", error);
res.status(500).json({ message: "Failed to fetch users" });
}
});
app.patch("/api/users/:id", isAuthenticated, async (req, res) => {
try {
const { role } = req.body;
if (!role || !["admin", "coordinator", "guard", "client"].includes(role)) {
return res.status(400).json({ message: "Invalid role" });
}
const updated = await storage.updateUserRole(req.params.id, role);
if (!updated) {
return res.status(404).json({ message: "User not found" });
}
res.json(updated);
} catch (error) {
console.error("Error updating user role:", error);
res.status(500).json({ message: "Failed to update user role" });
}
});
// ============= GUARD ROUTES =============
app.get("/api/guards", isAuthenticated, async (req, res) => {
try {

View File

@ -29,6 +29,8 @@ export interface IStorage {
// User operations (Replit Auth required)
getUser(id: string): Promise<User | undefined>;
upsertUser(user: UpsertUser): Promise<User>;
getAllUsers(): Promise<User[]>;
updateUserRole(id: string, role: "admin" | "coordinator" | "guard" | "client"): Promise<User | undefined>;
// Guard operations
getAllGuards(): Promise<Guard[]>;
@ -86,6 +88,19 @@ export class DatabaseStorage implements IStorage {
return user;
}
async getAllUsers(): Promise<User[]> {
return await db.select().from(users).orderBy(desc(users.createdAt));
}
async updateUserRole(id: string, role: "admin" | "coordinator" | "guard" | "client"): Promise<User | undefined> {
const [updated] = await db
.update(users)
.set({ role, updatedAt: new Date() })
.where(eq(users.id, id))
.returning();
return updated;
}
// Guard operations
async getAllGuards(): Promise<Guard[]> {
return await db.select().from(guards);