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_ID=front
|
||||||
KEYCLOAK_CLIENT_SECRET=Klsbm7hzyXscypXU0wUPPVBrttFPt6Pn
|
KEYCLOAK_CLIENT_SECRET=Klsbm7hzyXscypXU0wUPPVBrttFPt6Pn
|
||||||
KEYCLOAK_REALM=master
|
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: {
|
callbacks: {
|
||||||
async jwt({ token, account, profile }) {
|
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
|
// Au moment de la première connexion, sauvegarde de l'access token et du rôle dans le JWT
|
||||||
if (account && profile) {
|
if (account && profile) {
|
||||||
token.accessToken = account.access_token;
|
token.accessToken = account.access_token;
|
||||||
token.refreshToken = account.refresh_token;
|
token.refreshToken = account.refresh_token;
|
||||||
|
token.accessTokenExpires = account.expires_at! * 1000;
|
||||||
token.first_name = profile.given_name;
|
token.first_name = profile.given_name;
|
||||||
token.last_name = profile.family_name;
|
token.last_name = profile.family_name;
|
||||||
token.username = profile.preferred_username;
|
token.username = profile.preferred_username;
|
||||||
token.role = profile.realm_roles;
|
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 }) {
|
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
|
// On injecte l'access token et le rôle dans la session accessible côté client
|
||||||
session.accessToken = token.accessToken as string;
|
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.first_name = token.first_name as string;
|
||||||
session.user.last_name = token.last_name as string;
|
session.user.last_name = token.last_name as string;
|
||||||
session.user.username = token.username as string;
|
session.user.username = token.username as string;
|
||||||
@ -48,12 +94,11 @@ export const authOptions: NextAuthOptions = {
|
|||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
|
maxAge: 60 * 60, // 1 heure
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
async signOut({ token }) {
|
async signOut({ token }) {
|
||||||
try {
|
try {
|
||||||
console.log("Déconnexion Keycloak");
|
|
||||||
console.log("Token", token);
|
|
||||||
const issuerUrl = process.env.KEYCLOAK_ISSUER!;
|
const issuerUrl = process.env.KEYCLOAK_ISSUER!;
|
||||||
const logoutUrl = `${issuerUrl}/protocol/openid-connect/logout`;
|
const logoutUrl = `${issuerUrl}/protocol/openid-connect/logout`;
|
||||||
await fetch(logoutUrl, {
|
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 { cn } from "@/lib/utils";
|
||||||
import { LayoutGrid, BookOpen, Share2, Palette, GitFork, Building2, Users } from "lucide-react"
|
import {
|
||||||
import { Button } from "@/components/ui/button"
|
BookOpen,
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
Share2,
|
||||||
import { useRouter, usePathname } from "next/navigation"
|
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> {
|
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
isOpen: boolean
|
isOpen: boolean;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
title: "Board",
|
title: "Users",
|
||||||
icon: LayoutGrid,
|
icon: Users,
|
||||||
href: "/board",
|
href: "/users",
|
||||||
iframe: "https://example.com/board",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Chapter",
|
title: "Chapter",
|
||||||
@ -57,43 +64,48 @@ const menuItems = [
|
|||||||
href: "/missions",
|
href: "/missions",
|
||||||
iframe: "https://example.com/missions",
|
iframe: "https://example.com/missions",
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export function Sidebar({ isOpen, onClose, className }: SidebarProps) {
|
export function Sidebar({ isOpen, onClose, className }: SidebarProps) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
|
|
||||||
const handleNavigation = (href: string) => {
|
const handleNavigation = (href: string) => {
|
||||||
router.push(href)
|
router.push(href);
|
||||||
onClose()
|
onClose();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
className={cn(
|
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",
|
"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",
|
isOpen ? "translate-x-0" : "-translate-x-full",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ScrollArea className="h-full w-full">
|
<ScrollArea className='h-full w-full'>
|
||||||
<div className="space-y-1 p-4">
|
<div className='space-y-1 p-4'>
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<Button
|
<Button
|
||||||
key={item.title}
|
key={item.title}
|
||||||
variant="ghost"
|
variant='ghost'
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-start gap-2 text-white hover:bg-gray-800 hover:text-white",
|
"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)}
|
onClick={() => handleNavigation(item.href)}
|
||||||
>
|
>
|
||||||
<item.icon className="h-5 w-5" />
|
<item.icon className='h-5 w-5' />
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
{item.badge && (
|
{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}
|
{item.badge}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -103,6 +115,5 @@ export function Sidebar({ isOpen, onClose, className }: SidebarProps) {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</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[];
|
role: string[];
|
||||||
} & DefaultSession["user"];
|
} & DefaultSession["user"];
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JWT {
|
interface JWT {
|
||||||
@ -18,6 +20,7 @@ declare module "next-auth" {
|
|||||||
last_name?: string;
|
last_name?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
role?: string[] | string | null;
|
role?: string[] | string | null;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User extends DefaultUser {
|
interface User extends DefaultUser {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user