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:
Kevin 2025-02-21 19:30:47 +01:00
parent 11d3fe70ed
commit 74b9fd4dc3
6 changed files with 563 additions and 29 deletions

View 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 });
}
}

View 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
View 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>
);
}

View File

@ -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>
</>
)
);
}

View 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>
);
}

View 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>
)}
</>
);
}