From 8d4c9d5e23f5674b749023df717ea70f4babf245 Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 9 Jan 2026 16:50:21 +0100 Subject: [PATCH] Mission Refactor Members --- .env | 6 +- .../[missionId]/change-guardian/route.ts | 164 ++++++++++++ .../missions/[missionId]/remove-user/route.ts | 136 ++++++++++ components/missions/missions-admin-panel.tsx | 239 +++++++++++++++--- 4 files changed, 509 insertions(+), 36 deletions(-) create mode 100644 app/api/missions/[missionId]/change-guardian/route.ts create mode 100644 app/api/missions/[missionId]/remove-user/route.ts diff --git a/.env b/.env index 5fb499ee..be1f07f5 100644 --- a/.env +++ b/.env @@ -106,4 +106,8 @@ MICROSOFT_REDIRECT_URI="https://hub.slm-lab.net/ms" MICROSOFT_TENANT_ID="cb4281a9-4a3e-4ff5-9a85-8425dd04e2b2" N8N_WEBHOOK_URL="https://brain.slm-lab.net/webhook/mission-created" N8N_DELETE_WEBHOOK_URL="https://brain.slm-lab.net/webhook/mission-delete" -N8N_API_KEY=LwgeE1ntADD20OuWC88S3pR0EaO7FtO4 \ No newline at end of file +N8N_API_KEY=LwgeE1ntADD20OuWC88S3pR0EaO7FtO4 +N8N_GENERATE_PLAN_WEBHOOK_URL=https://brain.slm-lab.net/webhook/GeneratePlan +N8N_CLOSE_MISSION_WEBHOOK_URL=https://brain.slm-lab.net/webhook/NeahMissionClose +N8N_CHANGE_GUARDIAN_WEBHOOK_URL=https://brain.slm-lab.net/webhook/NeahMissionChangeGuardian +N8N_REMOVE_USER_WEBHOOK_URL=https://brain.slm-lab.net/webhook/NeahMissionRemoveUser \ No newline at end of file diff --git a/app/api/missions/[missionId]/change-guardian/route.ts b/app/api/missions/[missionId]/change-guardian/route.ts new file mode 100644 index 00000000..3b95095d --- /dev/null +++ b/app/api/missions/[missionId]/change-guardian/route.ts @@ -0,0 +1,164 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { logger } from '@/lib/logger'; + +export async function POST( + request: Request, + props: { params: Promise<{ missionId: string }> } +) { + const params = await props.params; + const { missionId } = params; + + try { + // Check authentication + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + const { guardianRole, oldUserId, newUserId, oldUserEmail, newUserEmail } = body; + + if (!guardianRole || !newUserId) { + return NextResponse.json( + { error: 'guardianRole and newUserId are required' }, + { status: 400 } + ); + } + + // Get mission details + const mission = await prisma.mission.findUnique({ + where: { id: missionId }, + select: { + id: true, + name: true, + creatorId: true, + leantimeProjectId: true, + outlineCollectionId: true, + rocketChatChannelId: true, + giteaRepositoryUrl: true, + }, + }); + + if (!mission) { + return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); + } + + // Check if user is authorized (creator or admin) + const isCreator = mission.creatorId === session.user.id; + const isAdmin = session.user.role?.includes('admin'); + + if (!isCreator && !isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + // Map guardian role to database role + const roleMap: Record = { + 'temps': 'temps', + 'parole': 'parole', + 'memoire': 'memoire', + }; + + const dbRole = roleMap[guardianRole]; + if (!dbRole) { + return NextResponse.json({ error: 'Invalid guardian role' }, { status: 400 }); + } + + // Update the guardian in database + // First, remove old guardian if exists + if (oldUserId) { + await prisma.missionUser.deleteMany({ + where: { + missionId, + userId: oldUserId, + role: dbRole, + }, + }); + } + + // Then add new guardian + await prisma.missionUser.create({ + data: { + missionId, + userId: newUserId, + role: dbRole, + }, + }); + + // Extract repo name from Gitea URL + let repoName = ''; + if (mission.giteaRepositoryUrl) { + try { + const url = new URL(mission.giteaRepositoryUrl); + const pathParts = url.pathname.split('/').filter(Boolean); + repoName = pathParts[pathParts.length - 1] || ''; + } catch { + const match = mission.giteaRepositoryUrl.match(/\/([^\/]+)\/?$/); + repoName = match ? match[1] : ''; + } + } + + // Prepare data for N8N webhook + const n8nData = { + missionId: mission.id, + missionName: mission.name, + guardianRole, + oldUserId, + newUserId, + oldUserEmail, + newUserEmail, + repoName, + leantimeProjectId: mission.leantimeProjectId || '', + outlineCollectionId: mission.outlineCollectionId || '', + rocketChatChannelId: mission.rocketChatChannelId || '', + giteaRepositoryUrl: mission.giteaRepositoryUrl || '', + }; + + // Call N8N webhook + const webhookUrl = process.env.N8N_CHANGE_GUARDIAN_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/NeahMissionChangeGuardian'; + const apiKey = process.env.N8N_API_KEY || ''; + + logger.debug('Calling N8N ChangeGuardian webhook', { + missionId, + guardianRole, + oldUserId, + newUserId, + }); + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + }, + body: JSON.stringify(n8nData), + }); + + if (!response.ok) { + const errorText = await response.text(); + logger.error('N8N ChangeGuardian webhook error', { + status: response.status, + error: errorText.substring(0, 200), + }); + // Continue even if N8N fails, database is already updated + } + + return NextResponse.json({ + success: true, + message: 'Guardian changed successfully', + }); + + } catch (error) { + logger.error('Error changing guardian', { + error: error instanceof Error ? error.message : String(error), + missionId, + }); + return NextResponse.json( + { error: 'Failed to change guardian', details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} + diff --git a/app/api/missions/[missionId]/remove-user/route.ts b/app/api/missions/[missionId]/remove-user/route.ts new file mode 100644 index 00000000..fcd3fca4 --- /dev/null +++ b/app/api/missions/[missionId]/remove-user/route.ts @@ -0,0 +1,136 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { logger } from '@/lib/logger'; + +export async function POST( + request: Request, + props: { params: Promise<{ missionId: string }> } +) { + const params = await props.params; + const { missionId } = params; + + try { + // Check authentication + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + const { userId, userEmail, role } = body; + + if (!userId) { + return NextResponse.json( + { error: 'userId is required' }, + { status: 400 } + ); + } + + // Get mission details + const mission = await prisma.mission.findUnique({ + where: { id: missionId }, + select: { + id: true, + name: true, + creatorId: true, + leantimeProjectId: true, + outlineCollectionId: true, + rocketChatChannelId: true, + giteaRepositoryUrl: true, + }, + }); + + if (!mission) { + return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); + } + + // Check if user is authorized (creator or admin) + const isCreator = mission.creatorId === session.user.id; + const isAdmin = session.user.role?.includes('admin'); + + if (!isCreator && !isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + // Remove user from mission in database + await prisma.missionUser.deleteMany({ + where: { + missionId, + userId, + }, + }); + + // Extract repo name from Gitea URL + let repoName = ''; + if (mission.giteaRepositoryUrl) { + try { + const url = new URL(mission.giteaRepositoryUrl); + const pathParts = url.pathname.split('/').filter(Boolean); + repoName = pathParts[pathParts.length - 1] || ''; + } catch { + const match = mission.giteaRepositoryUrl.match(/\/([^\/]+)\/?$/); + repoName = match ? match[1] : ''; + } + } + + // Prepare data for N8N webhook + const n8nData = { + missionId: mission.id, + missionName: mission.name, + userId, + userEmail, + role: role || 'volontaire', + repoName, + leantimeProjectId: mission.leantimeProjectId || '', + outlineCollectionId: mission.outlineCollectionId || '', + rocketChatChannelId: mission.rocketChatChannelId || '', + giteaRepositoryUrl: mission.giteaRepositoryUrl || '', + }; + + // Call N8N webhook + const webhookUrl = process.env.N8N_REMOVE_USER_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/NeahMissionRemoveUser'; + const apiKey = process.env.N8N_API_KEY || ''; + + logger.debug('Calling N8N RemoveUser webhook', { + missionId, + userId, + userEmail, + }); + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + }, + body: JSON.stringify(n8nData), + }); + + if (!response.ok) { + const errorText = await response.text(); + logger.error('N8N RemoveUser webhook error', { + status: response.status, + error: errorText.substring(0, 200), + }); + // Continue even if N8N fails, database is already updated + } + + return NextResponse.json({ + success: true, + message: 'User removed successfully', + }); + + } catch (error) { + logger.error('Error removing user from mission', { + error: error instanceof Error ? error.message : String(error), + missionId, + }); + return NextResponse.json( + { error: 'Failed to remove user', details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} + diff --git a/components/missions/missions-admin-panel.tsx b/components/missions/missions-admin-panel.tsx index 0653c13c..cb02d0c3 100644 --- a/components/missions/missions-admin-panel.tsx +++ b/components/missions/missions-admin-panel.tsx @@ -18,7 +18,7 @@ import { CardContent } from "../ui/card"; import { Badge } from "../ui/badge"; -import { X, Search, UserPlus, Users, PlusCircle, AlertCircle, Check, UploadCloud, File } from "lucide-react"; +import { X, Search, UserPlus, Users, PlusCircle, AlertCircle, Check, UploadCloud, File, RefreshCw } from "lucide-react"; import { toast } from "../ui/use-toast"; import { DropdownMenu, @@ -268,7 +268,7 @@ export function MissionsAdminPanel() { }); }; - // Function to remove a user from all roles + // Function to remove a user from all roles (local state only - for new missions) const removeUserFromAllRoles = (userId: string) => { if (gardienDuTemps === userId) setGardienDuTemps(null); if (gardienDeLaParole === userId) setGardienDeLaParole(null); @@ -277,6 +277,107 @@ export function MissionsAdminPanel() { setVolontaires(prev => prev.filter(id => id !== userId)); } }; + + // Function to change a guardian and call N8N webhook (for existing missions) + const handleChangeGuardian = async ( + guardianRole: 'temps' | 'parole' | 'memoire', + oldUserId: string | null, + newUserId: string + ) => { + const oldUser = oldUserId ? users.find(u => u.id === oldUserId) : null; + const newUser = users.find(u => u.id === newUserId); + + // Update local state first + if (guardianRole === 'temps') { + setGardienDuTemps(newUserId); + } else if (guardianRole === 'parole') { + setGardienDeLaParole(newUserId); + } else if (guardianRole === 'memoire') { + setGardienDeLaMemoire(newUserId); + } + + // If this is an existing mission, call the API + if (missionId) { + try { + const response = await fetch(`/api/missions/${missionId}/change-guardian`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + guardianRole, + oldUserId, + newUserId, + oldUserEmail: oldUser?.email, + newUserEmail: newUser?.email, + }), + }); + + if (!response.ok) { + throw new Error('Failed to change guardian'); + } + + toast({ + title: "Gardien modifié", + description: `Le gardien a été changé avec succès`, + }); + } catch (error) { + logger.error('Error changing guardian', { error }); + toast({ + title: "Erreur", + description: "Erreur lors du changement de gardien", + variant: "destructive", + }); + } + } else { + toast({ + title: "Gardien modifié", + description: `Le gardien a été changé`, + }); + } + }; + + // Function to remove a volontaire and call N8N webhook (for existing missions) + const handleRemoveVolontaire = async (userId: string) => { + const user = users.find(u => u.id === userId); + + // Update local state first + setVolontaires(prev => prev.filter(id => id !== userId)); + + // If this is an existing mission, call the API + if (missionId) { + try { + const response = await fetch(`/api/missions/${missionId}/remove-user`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId, + userEmail: user?.email, + role: 'volontaire', + }), + }); + + if (!response.ok) { + throw new Error('Failed to remove user'); + } + + toast({ + title: "Utilisateur supprimé", + description: `L'utilisateur a été retiré de la mission`, + }); + } catch (error) { + logger.error('Error removing user', { error }); + toast({ + title: "Erreur", + description: "Erreur lors de la suppression de l'utilisateur", + variant: "destructive", + }); + } + } else { + toast({ + title: "Utilisateur supprimé", + description: `L'utilisateur a été retiré`, + }); + } + }; // Check if all guardian roles are filled const areAllGuardiensFilled = (): boolean => { @@ -1121,16 +1222,38 @@ export function MissionsAdminPanel() {

Gardien du Temps

{gardienDuTemps && ( - + + + + + + {users.filter(u => u.id !== gardienDuTemps && u.id !== gardienDeLaParole && u.id !== gardienDeLaMemoire).map(user => ( + handleChangeGuardian('temps', gardienDuTemps, user.id)} + className="cursor-pointer hover:bg-gray-100" + > +
+
+ {user.firstName?.[0] || ""}{user.lastName?.[0] || ""} +
+
+
{user.firstName} {user.lastName}
+
{user.email}
+
+
+
+ ))} +
+
)}
{loading ? ( @@ -1177,16 +1300,38 @@ export function MissionsAdminPanel() {

Gardien de la Parole

{gardienDeLaParole && ( - + + + + + + {users.filter(u => u.id !== gardienDuTemps && u.id !== gardienDeLaParole && u.id !== gardienDeLaMemoire).map(user => ( + handleChangeGuardian('parole', gardienDeLaParole, user.id)} + className="cursor-pointer hover:bg-gray-100" + > +
+
+ {user.firstName?.[0] || ""}{user.lastName?.[0] || ""} +
+
+
{user.firstName} {user.lastName}
+
{user.email}
+
+
+
+ ))} +
+
)}
{loading ? ( @@ -1233,16 +1378,38 @@ export function MissionsAdminPanel() {

Gardien de la Mémoire

{gardienDeLaMemoire && ( - + + + + + + {users.filter(u => u.id !== gardienDuTemps && u.id !== gardienDeLaParole && u.id !== gardienDeLaMemoire).map(user => ( + handleChangeGuardian('memoire', gardienDeLaMemoire, user.id)} + className="cursor-pointer hover:bg-gray-100" + > +
+
+ {user.firstName?.[0] || ""}{user.lastName?.[0] || ""} +
+
+
{user.firstName} {user.lastName}
+
{user.email}
+
+
+
+ ))} +
+
)}
{loading ? ( @@ -1331,8 +1498,9 @@ export function MissionsAdminPanel() { @@ -1452,11 +1620,12 @@ export function MissionsAdminPanel() { Volontaire - {isUserAssigned(user.id) && ( + {/* Only show delete button for volontaires (not guardians) */} + {volontaires.includes(user.id) && (