mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-18 20:59:46 +02:00
Implement secure user authentication with role-based access control
Integrates user authentication using bcrypt, session management with PostgreSQL, and role-based access control for enhanced security and user management. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 91318273-c764-4fd4-be04-bdc12c38af32 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/ab880d72-1a34-4d36-b5be-9f44df85ed1a.jpg
This commit is contained in:
1 parent
42b249e5b0
commit
5c01234fc4
16 files changed
+2384
-827
No files matched your search
@@ -0,0 +1,456 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { insertUserSchema, insertStoreSchema, type User, type Store } from "@shared/schema";
|
||||
import { Users, Building2, Plus, Edit } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
|
||||
const createUserSchema = insertUserSchema.extend({
|
||||
confirmPassword: z.string(),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Les mots de passe ne correspondent pas",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
type CreateUserData = z.infer<typeof createUserSchema>;
|
||||
|
||||
export default function Administration() {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [isUserDialogOpen, setIsUserDialogOpen] = useState(false);
|
||||
const [isStoreDialogOpen, setIsStoreDialogOpen] = useState(false);
|
||||
|
||||
// Queries
|
||||
const { data: users = [], isLoading: usersLoading } = useQuery<User[]>({
|
||||
queryKey: ['/api/admin/users'],
|
||||
});
|
||||
|
||||
const { data: stores = [], isLoading: storesLoading } = useQuery<Store[]>({
|
||||
queryKey: ['/api/admin/stores'],
|
||||
});
|
||||
|
||||
// Mutations
|
||||
const createUserMutation = useMutation({
|
||||
mutationFn: (userData: CreateUserData) => {
|
||||
const { confirmPassword, ...data } = userData;
|
||||
return apiRequest('/api/admin/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/admin/users'] });
|
||||
setIsUserDialogOpen(false);
|
||||
toast({ title: "Utilisateur créé avec succès" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Erreur", description: "Impossible de créer l'utilisateur", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const createStoreMutation = useMutation({
|
||||
mutationFn: (storeData: z.infer<typeof insertStoreSchema>) =>
|
||||
apiRequest('/api/admin/stores', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(storeData),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/admin/stores'] });
|
||||
setIsStoreDialogOpen(false);
|
||||
toast({ title: "Magasin créé avec succès" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Erreur", description: "Impossible de créer le magasin", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
// Forms
|
||||
const userForm = useForm<CreateUserData>({
|
||||
resolver: zodResolver(createUserSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
role: "employee",
|
||||
storeId: undefined,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const storeForm = useForm<z.infer<typeof insertStoreSchema>>({
|
||||
resolver: zodResolver(insertStoreSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
address: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onCreateUser = (data: CreateUserData) => {
|
||||
createUserMutation.mutate(data);
|
||||
};
|
||||
|
||||
const onCreateStore = (data: z.infer<typeof insertStoreSchema>) => {
|
||||
createStoreMutation.mutate(data);
|
||||
};
|
||||
|
||||
const getRoleBadge = (role: string) => {
|
||||
const colors = {
|
||||
admin: "bg-red-100 text-red-800",
|
||||
manager: "bg-blue-100 text-blue-800",
|
||||
employee: "bg-green-100 text-green-800",
|
||||
};
|
||||
return colors[role as keyof typeof colors] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Administration</h2>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="users" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="users" className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
Utilisateurs
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="stores" className="flex items-center gap-2">
|
||||
<Building2 className="h-4 w-4" />
|
||||
Magasins
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
<CardTitle>Gestion des Utilisateurs</CardTitle>
|
||||
<Dialog open={isUserDialogOpen} onOpenChange={setIsUserDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nouvel Utilisateur
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Créer un Utilisateur</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...userForm}>
|
||||
<form onSubmit={userForm.handleSubmit(onCreateUser)} className="space-y-4">
|
||||
<FormField
|
||||
control={userForm.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nom d'utilisateur</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={userForm.control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Prénom</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={userForm.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nom</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={userForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={userForm.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Rôle</FormLabel>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="admin">Administrateur</SelectItem>
|
||||
<SelectItem value="manager">Manager</SelectItem>
|
||||
<SelectItem value="employee">Employé</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={userForm.control}
|
||||
name="storeId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Magasin</FormLabel>
|
||||
<Select value={field.value?.toString()} onValueChange={(value) => field.onChange(parseInt(value))}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionner un magasin" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{stores.map((store) => (
|
||||
<SelectItem key={store.id} value={store.id.toString()}>
|
||||
{store.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={userForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mot de passe</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={userForm.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirmer le mot de passe</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={createUserMutation.isPending}>
|
||||
{createUserMutation.isPending ? "Création..." : "Créer"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{usersLoading ? (
|
||||
<p>Chargement...</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Utilisateur</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Rôle</TableHead>
|
||||
<TableHead>Magasin</TableHead>
|
||||
<TableHead>Statut</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{user.firstName} {user.lastName}</p>
|
||||
<p className="text-sm text-gray-500">{user.username}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getRoleBadge(user.role)}`}>
|
||||
{user.role === 'admin' ? 'Administrateur' :
|
||||
user.role === 'manager' ? 'Manager' : 'Employé'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{stores.find(s => s.id === user.storeId)?.name || '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.isActive ? (
|
||||
<span className="text-green-600">Actif</span>
|
||||
) : (
|
||||
<span className="text-red-600">Inactif</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stores">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
<CardTitle>Gestion des Magasins</CardTitle>
|
||||
<Dialog open={isStoreDialogOpen} onOpenChange={setIsStoreDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nouveau Magasin
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Créer un Magasin</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...storeForm}>
|
||||
<form onSubmit={storeForm.handleSubmit(onCreateStore)} className="space-y-4">
|
||||
<FormField
|
||||
control={storeForm.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nom du magasin</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={storeForm.control}
|
||||
name="address"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Adresse</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={storeForm.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Téléphone</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={storeForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={createStoreMutation.isPending}>
|
||||
{createStoreMutation.isPending ? "Création..." : "Créer"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{storesLoading ? (
|
||||
<p>Chargement...</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nom</TableHead>
|
||||
<TableHead>Adresse</TableHead>
|
||||
<TableHead>Téléphone</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Statut</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{stores.map((store) => (
|
||||
<TableRow key={store.id}>
|
||||
<TableCell className="font-medium">{store.name}</TableCell>
|
||||
<TableCell>{store.address}</TableCell>
|
||||
<TableCell>{store.phone}</TableCell>
|
||||
<TableCell>{store.email}</TableCell>
|
||||
<TableCell>
|
||||
{store.isActive ? (
|
||||
<span className="text-green-600">Actif</span>
|
||||
) : (
|
||||
<span className="text-red-600">Inactif</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user