Ajout de la page des utilisateurs avec vérification de session, création d'une API pour la suppression d'utilisateur et ajout d'un bouton pour ajouter un nouvel utilisateur.
This commit is contained in:
parent
11d3fe70ed
commit
74b9fd4dc3
40
front/app/api/users/[id]/route.ts
Normal file
40
front/app/api/users/[id]/route.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function DELETE(
|
||||
req: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (
|
||||
!session?.user?.role?.includes("admin") &&
|
||||
!session?.user?.role?.includes("TEACHERS")
|
||||
) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
return NextResponse.json({ success: true });
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur suppression utilisateur" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
187
front/app/api/users/route.ts
Normal file
187
front/app/api/users/route.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Récupérer la liste des utilisateurs
|
||||
const usersResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const users = await usersResponse.json();
|
||||
|
||||
// Récupérer les rôles pour chaque utilisateur
|
||||
const usersWithRoles = await Promise.all(
|
||||
users.map(async (user: any) => {
|
||||
try {
|
||||
const rolesResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${user.id}/role-mappings/realm`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const roles = await rolesResponse.json();
|
||||
|
||||
// Suppression de "default-roles-master" qui est un rôle technique
|
||||
const filteredRoles = roles.filter(
|
||||
(role: any) => role.name !== "default-roles-master"
|
||||
);
|
||||
|
||||
return {
|
||||
...user,
|
||||
roles: filteredRoles.map((role: any) => role.name),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Erreur lors de la récupération des rôles pour l'utilisateur ${user.id}:`,
|
||||
error
|
||||
);
|
||||
return {
|
||||
...user,
|
||||
roles: [],
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json(usersWithRoles);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des utilisateurs:", error);
|
||||
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (
|
||||
!session?.user?.role?.includes("admin") &&
|
||||
!session?.user?.role?.includes("TEACHERS")
|
||||
) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await req.json();
|
||||
|
||||
// Formater les données pour Keycloak
|
||||
const keycloakUser = {
|
||||
username: data.username,
|
||||
enabled: true,
|
||||
emailVerified: true,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
realmRoles: data.realmRoles ? [data.realmRoles] : [],
|
||||
};
|
||||
|
||||
if (keycloakUser.realmRoles.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Veuillez sélectionner un rôle" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Créer l'utilisateur
|
||||
const createResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(keycloakUser),
|
||||
}
|
||||
);
|
||||
|
||||
if (!createResponse.ok) {
|
||||
const errorData = await createResponse.json();
|
||||
console.log(errorData);
|
||||
if (errorData.errorMessage?.includes("User exists with same username")) {
|
||||
return NextResponse.json(
|
||||
{ error: "Un utilisateur existe déjà avec ce nom d'utilisateur" },
|
||||
{ status: 400 }
|
||||
);
|
||||
} else if (
|
||||
errorData.errorMessage?.includes("User exists with same email")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Un utilisateur existe déjà avec cet email" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur création utilisateur" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Récupérer l'utilisateur créé
|
||||
const userResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users?username=${data.username}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const user = await userResponse.json();
|
||||
|
||||
console.log("Utilisateur créé:", user[0]);
|
||||
|
||||
if (keycloakUser.realmRoles.length > 0) {
|
||||
// Récupérer l'ID du rôle
|
||||
const roleResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/roles/${data.realmRoles}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const roleData = await roleResponse.json();
|
||||
|
||||
// Ajouter le rôle à l'utilisateur
|
||||
await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${user[0].id}/role-mappings/realm`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify([roleData]),
|
||||
}
|
||||
);
|
||||
|
||||
// Ajouter les realmRoles a l'utilisateur avant de le renvoyer
|
||||
user[0].roles = [keycloakUser.realmRoles];
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, user: user[0] });
|
||||
} catch (error) {
|
||||
console.error("Erreur complète:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur serveur", details: error },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
22
front/app/users/page.tsx
Normal file
22
front/app/users/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||
import { redirect } from "next/navigation";
|
||||
import { UsersTable } from "@/components/users/users-table";
|
||||
|
||||
export const metadata = {
|
||||
title: "Enkun - Utilisateurs",
|
||||
};
|
||||
|
||||
export default async function UsersPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
redirect("/signin");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='container mx-auto py-10'>
|
||||
<UsersTable userRole={session.user.role} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,24 +1,31 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import type React from "react"
|
||||
import type React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LayoutGrid, BookOpen, Share2, Palette, GitFork, Building2, Users } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { useRouter, usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
BookOpen,
|
||||
Share2,
|
||||
Palette,
|
||||
GitFork,
|
||||
Building2,
|
||||
Users,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
|
||||
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
title: "Board",
|
||||
icon: LayoutGrid,
|
||||
href: "/board",
|
||||
iframe: "https://example.com/board",
|
||||
title: "Users",
|
||||
icon: Users,
|
||||
href: "/users",
|
||||
},
|
||||
{
|
||||
title: "Chapter",
|
||||
@ -57,43 +64,48 @@ const menuItems = [
|
||||
href: "/missions",
|
||||
iframe: "https://example.com/missions",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
export function Sidebar({ isOpen, onClose, className }: SidebarProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const handleNavigation = (href: string) => {
|
||||
router.push(href)
|
||||
onClose()
|
||||
}
|
||||
router.push(href);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && <div className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm" onClick={onClose} />}
|
||||
{isOpen && (
|
||||
<div
|
||||
className='fixed inset-0 z-40 bg-background/80 backdrop-blur-sm'
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-0 left-0 z-50 h-full w-64 transform bg-black text-white transition-transform duration-200 ease-in-out",
|
||||
isOpen ? "translate-x-0" : "-translate-x-full",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="space-y-1 p-4">
|
||||
<ScrollArea className='h-full w-full'>
|
||||
<div className='space-y-1 p-4'>
|
||||
{menuItems.map((item) => (
|
||||
<Button
|
||||
key={item.title}
|
||||
variant="ghost"
|
||||
variant='ghost'
|
||||
className={cn(
|
||||
"w-full justify-start gap-2 text-white hover:bg-gray-800 hover:text-white",
|
||||
pathname === item.href && "bg-gray-800",
|
||||
pathname === item.href && "bg-gray-800"
|
||||
)}
|
||||
onClick={() => handleNavigation(item.href)}
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<item.icon className='h-5 w-5' />
|
||||
<span>{item.title}</span>
|
||||
{item.badge && (
|
||||
<span className="ml-auto flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-xs text-white">
|
||||
<span className='ml-auto flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-xs text-white'>
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
@ -103,6 +115,5 @@ export function Sidebar({ isOpen, onClose, className }: SidebarProps) {
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
135
front/components/users/add-user-button.tsx
Normal file
135
front/components/users/add-user-button.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { useContext, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
interface AddUserButtonProps {
|
||||
userRole: string[];
|
||||
handleAddUser: (newUser: any) => void;
|
||||
}
|
||||
|
||||
export function AddUserButton({ userRole, handleAddUser }: AddUserButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
username: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
realmRoles: "",
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const availableRoles = userRole.includes("admin")
|
||||
? ["admin", "TEACHERS", "STUDENTS"]
|
||||
: ["STUDENTS"];
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setFormData({
|
||||
username: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
realmRoles: "",
|
||||
});
|
||||
setError(null);
|
||||
handleAddUser(data.user);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la création:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Ajouter un utilisateur</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nouvel utilisateur</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className='space-y-4'>
|
||||
{error && <div className='text-red-500 text-sm'>{error}</div>}
|
||||
<Input
|
||||
placeholder="Nom d'utilisateur"
|
||||
value={formData.username}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, username: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Prénom'
|
||||
value={formData.firstName}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, firstName: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Nom'
|
||||
value={formData.lastName}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, lastName: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
type='email'
|
||||
placeholder='Email'
|
||||
value={formData.email}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, email: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={formData.realmRoles}
|
||||
onValueChange={(value) =>
|
||||
setFormData({ ...formData, realmRoles: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Sélectionner un rôle' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableRoles.map((role) => (
|
||||
<SelectItem key={role} value={role}>
|
||||
{role.charAt(0).toUpperCase() + role.slice(1).toLowerCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button type='submit' className='w-full'>
|
||||
Créer
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
139
front/components/users/users-table.tsx
Normal file
139
front/components/users/users-table.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { AddUserButton } from "./add-user-button";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
createdTimestamp: number;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
interface UsersTableProps {
|
||||
userRole: string[];
|
||||
}
|
||||
|
||||
export function UsersTable({ userRole }: UsersTableProps) {
|
||||
const { data: session } = useSession();
|
||||
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
const handleAddUser = (newUser: User) => {
|
||||
console.log("Nouvel utilisateur:", newUser);
|
||||
console.log("Utilisateurs actuels:", users);
|
||||
setUsers((prevUsers) => [...prevUsers, newUser]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/users");
|
||||
const data = await response.json();
|
||||
setUsers(data);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des utilisateurs:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const canDelete = (targetUserRole: string[]) => {
|
||||
if (userRole.includes("admin")) return true;
|
||||
if (userRole.includes("TEACHERS")) {
|
||||
return targetUserRole.includes("STUDENTS");
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleDelete = async (userId: string) => {
|
||||
try {
|
||||
await fetch(`/api/users/${userId}`, { method: "DELETE" });
|
||||
fetchUsers();
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la suppression:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const filterUsers = (users: User[]) => {
|
||||
if (userRole.includes("admin")) return users;
|
||||
if (userRole.includes("TEACHERS")) {
|
||||
return users.filter(
|
||||
(user) =>
|
||||
user.roles.includes("TEACHERS") || user.roles.includes("STUDENTS")
|
||||
);
|
||||
}
|
||||
return users.filter((user) => user.roles.includes("STUDENTS"));
|
||||
};
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex justify-between items-center mb-6'>
|
||||
<h1 className='text-2xl font-bold'>Gestion des utilisateurs</h1>
|
||||
{(session.user.role.includes("admin") ||
|
||||
session.user.role.includes("TEACHERS")) && (
|
||||
<AddUserButton
|
||||
userRole={session.user.role}
|
||||
handleAddUser={handleAddUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{users.length === 0 ? (
|
||||
<div className='text-center'>Chargement...</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nom d'utilisateur</TableHead>
|
||||
<TableHead>Prénom</TableHead>
|
||||
<TableHead>Nom</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Date d'inscription</TableHead>
|
||||
<TableHead>Roles</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filterUsers(users).map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>{user.firstName}</TableCell>
|
||||
<TableCell>{user.lastName}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>{user.createdTimestamp}</TableCell>
|
||||
<TableCell>{user.roles.join(", ")}</TableCell>
|
||||
<TableCell>
|
||||
{canDelete(user.roles) && (
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={() => handleDelete(user.id)}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user