diff --git a/front/app/api/users/[id]/route.ts b/front/app/api/users/[id]/route.ts new file mode 100644 index 0000000..746c9fa --- /dev/null +++ b/front/app/api/users/[id]/route.ts @@ -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 }); + } +} diff --git a/front/app/api/users/route.ts b/front/app/api/users/route.ts new file mode 100644 index 0000000..ac9b27d --- /dev/null +++ b/front/app/api/users/route.ts @@ -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 } + ); + } +} diff --git a/front/app/users/page.tsx b/front/app/users/page.tsx new file mode 100644 index 0000000..85fc73e --- /dev/null +++ b/front/app/users/page.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/front/components/sidebar.tsx b/front/components/sidebar.tsx index 59b9e68..26d4c92 100644 --- a/front/components/sidebar.tsx +++ b/front/components/sidebar.tsx @@ -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 { - 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 &&
} + {isOpen && ( +
+ )}
- -
+ +
{menuItems.map((item) => (
- ) + ); } - diff --git a/front/components/users/add-user-button.tsx b/front/components/users/add-user-button.tsx new file mode 100644 index 0000000..2db68a4 --- /dev/null +++ b/front/components/users/add-user-button.tsx @@ -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(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 ( + + + + + + + Nouvel utilisateur + +
+ {error &&
{error}
} + + setFormData({ ...formData, username: e.target.value }) + } + /> + + setFormData({ ...formData, firstName: e.target.value }) + } + /> + + setFormData({ ...formData, lastName: e.target.value }) + } + /> + + setFormData({ ...formData, email: e.target.value }) + } + /> + + +
+
+
+ ); +} diff --git a/front/components/users/users-table.tsx b/front/components/users/users-table.tsx new file mode 100644 index 0000000..b197b9d --- /dev/null +++ b/front/components/users/users-table.tsx @@ -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([]); + + 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 ( + <> +
+

Gestion des utilisateurs

+ {(session.user.role.includes("admin") || + session.user.role.includes("TEACHERS")) && ( + + )} +
+ {users.length === 0 ? ( +
Chargement...
+ ) : ( + + + + Nom d'utilisateur + Prénom + Nom + Email + Date d'inscription + Roles + Actions + + + + {filterUsers(users).map((user) => ( + + {user.username} + {user.firstName} + {user.lastName} + {user.email} + {user.createdTimestamp} + {user.roles.join(", ")} + + {canDelete(user.roles) && ( + + )} + + + ))} + +
+ )} + + ); +}