Compare commits
3 Commits
6018b853a0
...
74b9fd4dc3
| Author | SHA1 | Date | |
|---|---|---|---|
| 74b9fd4dc3 | |||
| 11d3fe70ed | |||
| cf6ea26879 |
@ -4,4 +4,5 @@ NEXTAUTH_SECRET=9eff5ad2f4b5ea744a34d9d8004cb5236f1931b26bf75f01a0a26203312fe1ec
|
||||
KEYCLOAK_CLIENT_ID=front
|
||||
KEYCLOAK_CLIENT_SECRET=Klsbm7hzyXscypXU0wUPPVBrttFPt6Pn
|
||||
KEYCLOAK_REALM=master
|
||||
KEYCLOAK_ISSUER=http://172.16.32.141:8090/realms/master
|
||||
KEYCLOAK_ISSUER=http://172.16.32.141:8090/realms/master
|
||||
KEYCLOAK_BASE_URL=http://172.16.32.141:8090
|
||||
@ -22,23 +22,69 @@ export const authOptions: NextAuthOptions = {
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, account, profile }) {
|
||||
console.log("Token", token);
|
||||
console.log("Account", account);
|
||||
console.log("Profile", profile);
|
||||
// Au moment de la première connexion, sauvegarde de l'access token et du rôle dans le JWT
|
||||
if (account && profile) {
|
||||
token.accessToken = account.access_token;
|
||||
token.refreshToken = account.refresh_token;
|
||||
token.accessTokenExpires = account.expires_at! * 1000;
|
||||
token.first_name = profile.given_name;
|
||||
token.last_name = profile.family_name;
|
||||
token.username = profile.preferred_username;
|
||||
token.role = profile.realm_roles;
|
||||
return token;
|
||||
}
|
||||
|
||||
// Retourner le token précédent si le token d'accès n'a pas expiré
|
||||
if (Date.now() < (token.accessTokenExpires as number)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Rafraîchir le token si expiré
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
||||
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||
refresh_token: token.refreshToken as string,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const refreshedTokens = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw refreshedTokens;
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
accessToken: refreshedTokens.access_token,
|
||||
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
|
||||
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du rafraîchissement du token:", error);
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError",
|
||||
};
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (token.error) {
|
||||
// Rediriger vers la page de connexion si le rafraîchissement a échoué
|
||||
throw new Error("RefreshAccessTokenError");
|
||||
}
|
||||
|
||||
// On injecte l'access token et le rôle dans la session accessible côté client
|
||||
session.accessToken = token.accessToken as string;
|
||||
session.refreshToken = token.refreshToken as string;
|
||||
session.error = token.error as string;
|
||||
session.user.first_name = token.first_name as string;
|
||||
session.user.last_name = token.last_name as string;
|
||||
session.user.username = token.username as string;
|
||||
@ -48,12 +94,11 @@ export const authOptions: NextAuthOptions = {
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 60 * 60, // 1 heure
|
||||
},
|
||||
events: {
|
||||
async signOut({ token }) {
|
||||
try {
|
||||
console.log("Déconnexion Keycloak");
|
||||
console.log("Token", token);
|
||||
const issuerUrl = process.env.KEYCLOAK_ISSUER!;
|
||||
const logoutUrl = `${issuerUrl}/protocol/openid-connect/logout`;
|
||||
await fetch(logoutUrl, {
|
||||
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
front/types/next-auth.d.ts
vendored
3
front/types/next-auth.d.ts
vendored
@ -10,6 +10,8 @@ declare module "next-auth" {
|
||||
role: string[];
|
||||
} & DefaultSession["user"];
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface JWT {
|
||||
@ -18,6 +20,7 @@ declare module "next-auth" {
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
role?: string[] | string | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface User extends DefaultUser {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user