Refactir logging mission deep

This commit is contained in:
alma 2026-01-07 11:52:55 +01:00
parent fbb8eac162
commit dce71a2cb3
13 changed files with 337 additions and 241 deletions

View File

@ -1,6 +1,7 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/options";
import { NextResponse } from "next/server";
import { logger } from '@/lib/logger';
async function getAdminToken() {
try {
@ -21,25 +22,28 @@ async function getAdminToken() {
const data = await tokenResponse.json();
// Log the response for debugging
console.log('Token Response:', {
// Log the response for debugging (without exposing the full token!)
logger.debug('Token Response', {
status: tokenResponse.status,
ok: tokenResponse.ok,
data: data
hasToken: !!data.access_token,
expiresIn: data.expires_in
});
if (!tokenResponse.ok || !data.access_token) {
// Log the error details
console.error('Token Error Details:', {
// Log the error details (without sensitive data)
logger.error('Token Error Details', {
status: tokenResponse.status,
data: data
error: data.error || 'Unknown error'
});
return null;
}
return data.access_token;
} catch (error) {
console.error('Token Error:', error);
logger.error('Token Error', {
error: error instanceof Error ? error.message : String(error)
});
return null;
}
}
@ -112,7 +116,9 @@ export async function GET() {
return NextResponse.json(groupsWithCounts);
} catch (error) {
console.error('Groups API Error:', error);
logger.error('Groups API Error', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json({ message: "Une erreur est survenue" }, { status: 500 });
}
}
@ -157,7 +163,9 @@ export async function POST(req: Request) {
membersCount: 0
});
} catch (error) {
console.error('Create Group Error:', error);
logger.error('Create Group Error', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json(
{ message: error instanceof Error ? error.message : "Une erreur est survenue" },
{ status: 500 }

View File

@ -3,15 +3,15 @@ import { getServerSession } from 'next-auth';
import { authOptions } from "@/app/api/auth/options";
import { prisma } from '@/lib/prisma';
import { deleteMissionAttachment } from '@/lib/mission-uploads';
import { logger } from '@/lib/logger';
// Helper function to check authentication
async function checkAuth(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
console.error('Unauthorized access attempt:', {
logger.error('Unauthorized access attempt', {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers)
method: request.method
});
return { authorized: false, userId: null };
}
@ -72,7 +72,11 @@ export async function DELETE(
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting attachment:', error);
logger.error('Error deleting attachment', {
error: error instanceof Error ? error.message : String(error),
missionId: params.missionId,
attachmentId: params.attachmentId
});
return NextResponse.json({
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error)

View File

@ -5,15 +5,15 @@ import { prisma } from '@/lib/prisma';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { s3Client, S3_CONFIG } from '@/lib/s3';
import { logger } from '@/lib/logger';
// Helper function to check authentication
async function checkAuth(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
console.error('Unauthorized access attempt:', {
logger.error('Unauthorized access attempt', {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers)
method: request.method
});
return { authorized: false, userId: null };
}
@ -76,7 +76,11 @@ export async function GET(
// Redirect the user to the presigned URL for direct download
return NextResponse.redirect(url);
} catch (error) {
console.error('Error downloading attachment:', error);
logger.error('Error downloading attachment', {
error: error instanceof Error ? error.message : String(error),
missionId: params.missionId,
attachmentId: params.attachmentId
});
return NextResponse.json({
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error)

View File

@ -3,15 +3,15 @@ import { getServerSession } from 'next-auth';
import { authOptions } from "@/app/api/auth/options";
import { prisma } from '@/lib/prisma';
import { getPublicUrl, S3_CONFIG } from '@/lib/s3';
import { logger } from '@/lib/logger';
// Helper function to check authentication
async function checkAuth(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
console.error('Unauthorized access attempt:', {
logger.error('Unauthorized access attempt', {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers)
method: request.method
});
return { authorized: false, userId: null };
}
@ -69,7 +69,10 @@ export async function GET(request: Request, props: { params: Promise<{ missionId
return NextResponse.json(attachmentsWithUrls);
} catch (error) {
console.error('Error fetching mission attachments:', error);
logger.error('Error fetching mission attachments', {
error: error instanceof Error ? error.message : String(error),
missionId: params.missionId
});
return NextResponse.json({
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error)

View File

@ -5,15 +5,15 @@ import { prisma } from '@/lib/prisma';
import { deleteMissionLogo, deleteMissionAttachment, getMissionFileUrl } from '@/lib/mission-uploads';
import { getPublicUrl, S3_CONFIG } from '@/lib/s3';
import { N8nService } from '@/lib/services/n8n-service';
import { logger } from '@/lib/logger';
// Helper function to check authentication
async function checkAuth(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
console.error('Unauthorized access attempt:', {
logger.error('Unauthorized access attempt', {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers)
method: request.method
});
return { authorized: false, userId: null };
}
@ -91,20 +91,17 @@ export async function GET(request: Request, props: { params: Promise<{ missionId
}))
};
console.log('Mission data with URLs:', {
logger.debug('Mission data with URLs', {
missionId: mission.id,
logoPath: mission.logo,
logoUrl: missionWithUrls.logoUrl,
hasLogo: !!mission.logo,
attachmentCount: mission.attachments.length
});
return NextResponse.json(missionWithUrls);
} catch (error) {
console.error('Error retrieving mission:', {
error,
missionId: params.missionId,
errorType: error instanceof Error ? error.constructor.name : typeof error,
message: error instanceof Error ? error.message : String(error)
logger.error('Error retrieving mission', {
error: error instanceof Error ? error.message : String(error),
missionId: params.missionId
});
return NextResponse.json({
error: 'Internal server error',
@ -174,9 +171,8 @@ export async function PUT(request: Request, props: { params: Promise<{ missionId
logoPath = `missions/${logo}`;
}
} catch (error) {
console.error('Error processing logo URL:', {
error,
logo,
logger.error('Error processing logo URL', {
error: error instanceof Error ? error.message : String(error),
missionId
});
}
@ -280,7 +276,10 @@ export async function PUT(request: Request, props: { params: Promise<{ missionId
}
});
} catch (error) {
console.error('Error updating mission:', error);
logger.error('Error updating mission', {
error: error instanceof Error ? error.message : String(error),
missionId: params.missionId
});
return NextResponse.json({
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error)
@ -329,7 +328,7 @@ export async function DELETE(
});
// Step 1: Trigger N8N workflow for deletion (rollback external integrations)
console.log('=== Starting N8N Deletion Workflow ===');
logger.debug('Starting N8N deletion workflow');
const n8nService = new N8nService();
// Extract repo name from giteaRepositoryUrl if present
@ -341,9 +340,11 @@ export async function DELETE(
// Extract repo name from path (last segment)
const pathParts = url.pathname.split('/').filter(Boolean);
repoName = pathParts[pathParts.length - 1] || '';
console.log('Extracted repo name from URL:', { url: mission.giteaRepositoryUrl, repoName });
logger.debug('Extracted repo name from URL', { repoName });
} catch (error) {
console.error('Error extracting repo name from URL:', error);
logger.error('Error extracting repo name from URL', {
error: error instanceof Error ? error.message : String(error)
});
// If URL parsing fails, try to extract from the string directly
const match = mission.giteaRepositoryUrl.match(/\/([^\/]+)\/?$/);
repoName = match ? match[1] : '';
@ -370,13 +371,22 @@ export async function DELETE(
}
};
console.log('Sending deletion data to N8N:', JSON.stringify(n8nDeletionData, null, 2));
logger.debug('Sending deletion data to N8N', {
missionId: n8nDeletionData.missionId,
name: n8nDeletionData.name,
hasRepoName: !!n8nDeletionData.repoName
});
const n8nResult = await n8nService.triggerMissionDeletion(n8nDeletionData);
console.log('N8N Deletion Workflow Result:', JSON.stringify(n8nResult, null, 2));
logger.debug('N8N deletion workflow result', {
success: n8nResult.success,
hasError: !!n8nResult.error
});
if (!n8nResult.success) {
console.error('N8N deletion workflow failed, but continuing with mission deletion:', n8nResult.error);
logger.error('N8N deletion workflow failed, but continuing with mission deletion', {
error: n8nResult.error
});
// Continue with deletion even if N8N fails (non-blocking)
}
@ -385,22 +395,28 @@ export async function DELETE(
if (mission.logo) {
try {
await deleteMissionLogo(params.missionId, mission.logo);
console.log('Logo deleted successfully from Minio');
logger.debug('Logo deleted successfully from Minio');
} catch (error) {
console.error('Error deleting mission logo from Minio:', error);
logger.error('Error deleting mission logo from Minio', {
error: error instanceof Error ? error.message : String(error),
missionId: params.missionId
});
// Continue deletion even if logo deletion fails
}
}
// Delete attachments from Minio
if (attachments.length > 0) {
console.log(`Deleting ${attachments.length} attachment(s) from Minio...`);
logger.debug(`Deleting ${attachments.length} attachment(s) from Minio`);
for (const attachment of attachments) {
try {
await deleteMissionAttachment(attachment.filePath);
console.log(`Attachment deleted successfully: ${attachment.filename}`);
logger.debug('Attachment deleted successfully', { filename: attachment.filename });
} catch (error) {
console.error(`Error deleting attachment ${attachment.filename} from Minio:`, error);
logger.error('Error deleting attachment from Minio', {
error: error instanceof Error ? error.message : String(error),
filename: attachment.filename
});
// Continue deletion even if one attachment fails
}
}
@ -411,11 +427,14 @@ export async function DELETE(
where: { id: params.missionId }
});
console.log('Mission deleted successfully from database');
logger.debug('Mission deleted successfully from database', { missionId: params.missionId });
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting mission:', error);
logger.error('Error deleting mission', {
error: error instanceof Error ? error.message : String(error),
missionId: params.missionId
});
return NextResponse.json(
{ error: 'Failed to delete mission' },
{ status: 500 }

View File

@ -4,15 +4,15 @@ import { authOptions } from "@/app/api/auth/options";
import { prisma } from '@/lib/prisma';
import { getPublicUrl } from '@/lib/s3';
import { S3_CONFIG } from '@/lib/s3';
import { logger } from '@/lib/logger';
// Helper function to check authentication
async function checkAuth(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
console.error('Unauthorized access attempt:', {
logger.error('Unauthorized access attempt', {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers)
method: request.method
});
return { authorized: false, userId: null };
}
@ -87,10 +87,9 @@ export async function GET(request: Request) {
// Transform missions to include public URLs (same format as /api/missions)
const missionsWithPublicUrls = missions.map(mission => {
console.log('Processing mission logo:', {
logger.debug('Processing mission logo', {
missionId: mission.id,
logo: mission.logo,
constructedUrl: mission.logo ? `/api/missions/image/${mission.logo}` : null
hasLogo: !!mission.logo
});
return {
@ -109,7 +108,9 @@ export async function GET(request: Request) {
}
});
} catch (error) {
console.error('Error listing all missions:', error);
logger.error('Error listing all missions', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json({
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error)

View File

@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/options';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { NoSuchKey } from '@aws-sdk/client-s3';
import { logger } from '@/lib/logger';
// Initialize S3 client
const s3Client = new S3Client({
@ -23,23 +24,22 @@ export async function GET(
try {
const { path: pathSegments } = await params;
if (!pathSegments || pathSegments.length === 0) {
console.error('No path segments provided');
logger.error('No path segments provided');
return new NextResponse('Path is required', { status: 400 });
}
// Reconstruct the full path from path segments
const filePath = pathSegments.join('/');
console.log('Fetching mission image:', {
logger.debug('Fetching mission image', {
originalPath: filePath,
segments: pathSegments
});
// Remove the missions/ prefix from the URL path since the file is already in the missions bucket
const minioPath = filePath.replace(/^missions\//, '');
console.log('Full Minio path:', {
logger.debug('Full Minio path', {
minioPath,
bucket: 'missions',
endpoint: 'https://dome-api.slm-lab.net'
bucket: 'missions'
});
const command = new GetObjectCommand({
@ -50,11 +50,10 @@ export async function GET(
try {
const response = await s3Client.send(command);
if (!response.Body) {
console.error('File not found in Minio:', {
logger.error('File not found in Minio', {
path: filePath,
minioPath,
bucket: 'missions',
contentType: response.ContentType
bucket: 'missions'
});
return new NextResponse('File not found', { status: 404 });
}
@ -65,24 +64,20 @@ export async function GET(
headers.set('Content-Type', contentType);
headers.set('Cache-Control', 'public, max-age=31536000');
// Log the response details
console.log('Serving image:', {
logger.debug('Serving image', {
path: filePath,
minioPath,
contentType,
contentLength: response.ContentLength,
lastModified: response.LastModified,
etag: response.ETag
contentLength: response.ContentLength
});
return new NextResponse(response.Body as any, { headers });
} catch (error) {
console.error('Error fetching file from Minio:', {
error,
logger.error('Error fetching file from Minio', {
error: error instanceof Error ? error.message : String(error),
path: filePath,
minioPath,
bucket: 'missions',
endpoint: 'https://dome-api.slm-lab.net',
errorType: error instanceof NoSuchKey ? 'NoSuchKey' : 'Unknown'
});
if (error instanceof NoSuchKey) {
@ -91,10 +86,8 @@ export async function GET(
return new NextResponse('Internal Server Error', { status: 500 });
}
} catch (error) {
console.error('Error in image serving:', {
error,
errorType: error instanceof Error ? error.constructor.name : typeof error,
message: error instanceof Error ? error.message : String(error)
logger.error('Error in image serving', {
error: error instanceof Error ? error.message : String(error)
});
return new NextResponse('Internal Server Error', { status: 500 });
}

View File

@ -1,5 +1,6 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { logger } from '@/lib/logger';
/**
* POST /api/missions/mission-created
@ -24,14 +25,14 @@ import { prisma } from '@/lib/prisma';
*/
export async function POST(request: Request) {
try {
console.log('=== Mission Created Webhook Received ===');
logger.debug('Mission Created Webhook Received');
// Vérifier l'API key
const apiKey = request.headers.get('x-api-key');
const expectedApiKey = process.env.N8N_API_KEY;
if (!expectedApiKey) {
console.error('N8N_API_KEY not configured in environment');
logger.error('N8N_API_KEY not configured in environment');
return NextResponse.json(
{ error: 'Server configuration error' },
{ status: 500 }
@ -39,7 +40,7 @@ export async function POST(request: Request) {
}
if (apiKey !== expectedApiKey) {
console.error('Invalid API key:', {
logger.error('Invalid API key', {
received: apiKey ? 'present' : 'missing',
expected: expectedApiKey ? 'configured' : 'missing'
});
@ -50,7 +51,11 @@ export async function POST(request: Request) {
}
const body = await request.json();
console.log('Received mission-created data:', JSON.stringify(body, null, 2));
logger.debug('Received mission-created data', {
hasMissionId: !!body.missionId,
hasName: !!body.name,
hasCreatorId: !!body.creatorId
});
// Validation des champs requis
// Prefer missionId if provided, otherwise use name + creatorId
@ -58,13 +63,13 @@ export async function POST(request: Request) {
if (body.missionId) {
// ✅ Use missionId if provided (more reliable)
console.log('Looking up mission by ID:', body.missionId);
logger.debug('Looking up mission by ID', { missionId: body.missionId });
mission = await prisma.mission.findUnique({
where: { id: body.missionId }
});
} else if (body.name && body.creatorId) {
// Fallback to name + creatorId (for backward compatibility)
console.log('Looking up mission by name + creatorId:', {
logger.debug('Looking up mission by name + creatorId', {
name: body.name,
creatorId: body.creatorId
});
@ -78,7 +83,7 @@ export async function POST(request: Request) {
}
});
} else {
console.error('Missing required fields:', {
logger.error('Missing required fields', {
hasMissionId: !!body.missionId,
hasName: !!body.name,
hasCreatorId: !!body.creatorId
@ -90,7 +95,7 @@ export async function POST(request: Request) {
}
if (!mission) {
console.error('Mission not found:', {
logger.error('Mission not found', {
missionId: body.missionId,
name: body.name,
creatorId: body.creatorId
@ -101,14 +106,14 @@ export async function POST(request: Request) {
);
}
console.log('Found mission:', {
logger.debug('Found mission', {
id: mission.id,
name: mission.name,
currentIntegrationIds: {
gitea: mission.giteaRepositoryUrl,
leantime: mission.leantimeProjectId,
outline: mission.outlineCollectionId,
rocketChat: mission.rocketChatChannelId
hasIntegrations: {
gitea: !!mission.giteaRepositoryUrl,
leantime: !!mission.leantimeProjectId,
outline: !!mission.outlineCollectionId,
rocketChat: !!mission.rocketChatChannelId
}
});
@ -123,7 +128,7 @@ export async function POST(request: Request) {
// Mapper les champs N8N vers notre schéma Prisma
if (body.gitRepoUrl !== undefined) {
updateData.giteaRepositoryUrl = body.gitRepoUrl || null;
console.log('Updating giteaRepositoryUrl:', body.gitRepoUrl);
logger.debug('Updating giteaRepositoryUrl', { hasUrl: !!body.gitRepoUrl });
}
if (body.leantimeProjectId !== undefined) {
@ -131,22 +136,22 @@ export async function POST(request: Request) {
updateData.leantimeProjectId = body.leantimeProjectId
? String(body.leantimeProjectId)
: null;
console.log('Updating leantimeProjectId:', updateData.leantimeProjectId);
logger.debug('Updating leantimeProjectId', { hasId: !!updateData.leantimeProjectId });
}
if (body.documentationCollectionId !== undefined) {
updateData.outlineCollectionId = body.documentationCollectionId || null;
console.log('Updating outlineCollectionId:', updateData.outlineCollectionId);
logger.debug('Updating outlineCollectionId', { hasId: !!updateData.outlineCollectionId });
}
if (body.rocketchatChannelId !== undefined) {
updateData.rocketChatChannelId = body.rocketchatChannelId || null;
console.log('Updating rocketChatChannelId:', updateData.rocketChatChannelId);
logger.debug('Updating rocketChatChannelId', { hasId: !!updateData.rocketChatChannelId });
}
// Vérifier qu'il y a au moins un champ à mettre à jour
if (Object.keys(updateData).length === 0) {
console.warn('No integration IDs to update');
logger.warn('No integration IDs to update');
return NextResponse.json({
message: 'Mission found but no integration IDs provided',
mission: {
@ -162,16 +167,10 @@ export async function POST(request: Request) {
data: updateData
});
console.log('Mission updated successfully:', {
logger.debug('Mission updated successfully', {
id: updatedMission.id,
name: updatedMission.name,
updatedFields: Object.keys(updateData),
newIntegrationIds: {
gitea: updatedMission.giteaRepositoryUrl,
leantime: updatedMission.leantimeProjectId,
outline: updatedMission.outlineCollectionId,
rocketChat: updatedMission.rocketChatChannelId
}
updatedFields: Object.keys(updateData)
});
return NextResponse.json({
@ -188,7 +187,9 @@ export async function POST(request: Request) {
});
} catch (error) {
console.error('Error in mission-created webhook:', error);
logger.error('Error in mission-created webhook', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json(
{
error: 'Failed to update mission',

View File

@ -7,6 +7,7 @@ import { Prisma } from '@prisma/client';
import { s3Client } from '@/lib/s3';
import { CopyObjectCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
import { uploadMissionLogo, uploadMissionAttachment, getMissionFileUrl } from '@/lib/mission-uploads';
import { logger } from '@/lib/logger';
// Types
interface MissionCreateInput {
@ -153,9 +154,9 @@ export async function GET(request: Request) {
// Transform missions to include public URLs
const missionsWithUrls = missions.map(mission => {
console.log('Processing mission logo:', {
logger.debug('Processing mission logo:', {
missionId: mission.id,
logo: mission.logo,
hasLogo: !!mission.logo,
constructedUrl: mission.logo ? `/api/missions/image/${mission.logo}` : null
});
@ -179,7 +180,9 @@ export async function GET(request: Request) {
}
});
} catch (error) {
console.error('Error listing missions:', error);
logger.error('Error listing missions', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json({
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error)
@ -196,7 +199,10 @@ async function verifyFileExists(filePath: string): Promise<boolean> {
}));
return true;
} catch (error) {
console.error('Error verifying file:', filePath, error);
logger.error('Error verifying file:', {
filePath,
error: error instanceof Error ? error.message : String(error)
});
return false;
}
}
@ -206,14 +212,19 @@ export async function POST(request: Request) {
let uploadedFiles: { type: 'logo' | 'attachment', path: string }[] = [];
try {
console.log('=== Mission Creation Started ===');
logger.debug('Mission creation started');
const { authorized, userId } = await checkAuth(request);
if (!authorized || !userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
console.log('Received request body:', JSON.stringify(body, null, 2));
logger.debug('Received mission creation request', {
hasName: !!body.name,
hasOddScope: !!body.oddScope,
hasLogo: !!body.logo?.data,
attachmentsCount: body.attachments?.length || 0
});
// Simple validation
if (!body.name || !body.oddScope) {
@ -239,13 +250,21 @@ export async function POST(request: Request) {
logo: null, // Will update after upload
};
console.log('Creating mission with data:', JSON.stringify(missionData, null, 2));
logger.debug('Creating mission', {
name: missionData.name,
oddScope: missionData.oddScope,
niveau: missionData.niveau,
missionType: missionData.missionType
});
const mission = await prisma.mission.create({
data: missionData
});
console.log('Mission created successfully:', JSON.stringify(mission, null, 2));
logger.debug('Mission created successfully', {
missionId: mission.id,
name: mission.name
});
// Step 2: Create mission users (guardians and volunteers)
const missionUsers = [];
@ -279,7 +298,10 @@ export async function POST(request: Request) {
await prisma.missionUser.createMany({
data: missionUsers
});
console.log('Mission users created:', missionUsers);
logger.debug('Mission users created', {
count: missionUsers.length,
roles: missionUsers.map(u => u.role)
});
}
// Step 3: Upload logo to Minio if present
@ -311,13 +333,15 @@ export async function POST(request: Request) {
data: { logo: filePath }
});
console.log('Logo uploaded successfully:', {
logger.debug('Logo uploaded successfully', {
logoPath,
logoUrl,
baseUrl
hasLogoUrl: !!logoUrl
});
} catch (uploadError) {
console.error('Error uploading logo:', uploadError);
logger.error('Error uploading logo', {
error: uploadError instanceof Error ? uploadError.message : String(uploadError),
missionId: mission.id
});
throw new Error('Failed to upload logo');
}
}
@ -348,9 +372,14 @@ export async function POST(request: Request) {
});
await Promise.all(attachmentPromises);
console.log('Attachments uploaded successfully');
logger.debug('Attachments uploaded successfully', {
count: body.attachments.length
});
} catch (attachmentError) {
console.error('Error uploading attachments:', attachmentError);
logger.error('Error uploading attachments', {
error: attachmentError instanceof Error ? attachmentError.message : String(attachmentError),
missionId: mission.id
});
throw new Error('Failed to upload attachments');
}
}
@ -378,7 +407,7 @@ export async function POST(request: Request) {
}
// Only trigger n8n after verifying all files
console.log('=== Starting N8N Workflow ===');
logger.debug('Starting N8N workflow');
const n8nService = new N8nService();
const n8nData = {
@ -392,10 +421,17 @@ export async function POST(request: Request) {
MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL
}
};
console.log('Sending to N8N:', JSON.stringify(n8nData, null, 2));
logger.debug('Sending to N8N', {
missionId: n8nData.missionId,
name: n8nData.name,
hasLogo: !!n8nData.logoPath
});
const workflowResult = await n8nService.triggerMissionCreation(n8nData);
console.log('N8N Workflow Result:', JSON.stringify(workflowResult, null, 2));
logger.debug('N8N workflow result', {
success: workflowResult.success,
hasError: !!workflowResult.error
});
if (!workflowResult.success) {
throw new Error(workflowResult.error || 'N8N workflow failed');
@ -407,11 +443,16 @@ export async function POST(request: Request) {
message: 'Mission created successfully with all integrations'
});
} catch (error) {
console.error('Error in final verification or n8n:', error);
logger.error('Error in final verification or n8n', {
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
} catch (error) {
console.error('Error in mission creation:', error);
logger.error('Error in mission creation', {
error: error instanceof Error ? error.message : String(error),
uploadedFilesCount: uploadedFiles.length
});
// Cleanup: Delete any uploaded files
for (const file of uploadedFiles) {
@ -420,9 +461,12 @@ export async function POST(request: Request) {
Bucket: 'missions',
Key: file.path.replace('missions/', '')
}));
console.log('Cleaned up file:', file.path);
logger.debug('Cleaned up file', { path: file.path });
} catch (cleanupError) {
console.error('Error cleaning up file:', file.path, cleanupError);
logger.error('Error cleaning up file', {
path: file.path,
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
});
}
}

View File

@ -10,15 +10,15 @@ import {
generateMissionAttachmentUploadUrl
} from '@/lib/mission-uploads';
import { getPublicUrl, S3_CONFIG } from '@/lib/s3';
import { logger } from '@/lib/logger';
// Helper function to check authentication
async function checkAuth(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
console.error('Unauthorized access attempt:', {
logger.error('Unauthorized access attempt', {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers)
method: request.method
});
return { authorized: false, userId: null };
}
@ -75,7 +75,9 @@ export async function GET(request: Request) {
return NextResponse.json(result);
} catch (error) {
console.error('Error generating upload URL:', error);
logger.error('Error generating upload URL', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json({
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error)
@ -85,25 +87,21 @@ export async function GET(request: Request) {
// Handle file upload (server-side)
export async function POST(request: Request) {
console.log('=== File upload request received ===');
logger.debug('File upload request received');
try {
console.log('Checking authentication...');
const { authorized, userId } = await checkAuth(request);
if (!authorized || !userId) {
console.log('Authentication failed');
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
console.log('User authenticated:', userId);
// Parse the form data
console.log('Parsing form data...');
const formData = await request.formData();
const missionId = formData.get('missionId') as string;
const type = formData.get('type') as string; // 'logo' or 'attachment'
const file = formData.get('file') as File;
console.log('Form data received:', {
logger.debug('Form data received', {
missionId,
type,
fileExists: !!file,
@ -113,7 +111,7 @@ export async function POST(request: Request) {
});
if (!missionId || !type || !file) {
console.log('Missing required fields:', { missionId: !!missionId, type: !!type, file: !!file });
logger.error('Missing required fields', { missionId: !!missionId, type: !!type, file: !!file });
return NextResponse.json({
error: 'Missing required fields',
required: { missionId: true, type: true, file: true },
@ -122,42 +120,38 @@ export async function POST(request: Request) {
}
// Verify that the mission exists and user has access to it
console.log('Verifying mission access...');
const mission = await prisma.mission.findUnique({
where: { id: missionId },
select: { id: true, creatorId: true }
});
if (!mission) {
console.log('Mission not found:', missionId);
logger.error('Mission not found', { missionId });
return NextResponse.json({ error: 'Mission not found' }, { status: 404 });
}
// Currently only allow creator to upload files
if (mission.creatorId !== userId) {
console.log('User not authorized to upload to this mission', { userId, creatorId: mission.creatorId });
logger.error('User not authorized to upload to this mission', { userId, creatorId: mission.creatorId });
return NextResponse.json({ error: 'Not authorized to upload to this mission' }, { status: 403 });
}
console.log('Mission access verified');
if (type === 'logo') {
console.log('Processing logo upload...');
logger.debug('Processing logo upload');
try {
// Upload logo file to Minio
const { filePath } = await uploadMissionLogo(userId, missionId, file);
console.log('Logo uploaded successfully to path:', filePath);
logger.debug('Logo uploaded successfully', { filePath });
// Generate public URL - remove missions/ prefix since it's added by the API
const publicUrl = `/api/missions/image/${filePath.replace('missions/', '')}`;
console.log('Public URL for logo:', publicUrl);
// Update mission record with logo path
console.log('Updating mission record with logo path...');
await prisma.mission.update({
where: { id: missionId },
data: { logo: filePath }
});
console.log('Mission record updated');
logger.debug('Mission record updated with logo');
return NextResponse.json({
success: true,
@ -165,7 +159,10 @@ export async function POST(request: Request) {
publicUrl
});
} catch (logoError) {
console.error('Error in logo upload process:', logoError);
logger.error('Error in logo upload process', {
error: logoError instanceof Error ? logoError.message : String(logoError),
missionId
});
return NextResponse.json({
error: 'Logo upload failed',
details: logoError instanceof Error ? logoError.message : String(logoError)
@ -174,21 +171,19 @@ export async function POST(request: Request) {
}
else if (type === 'attachment') {
// Upload attachment file to Minio
console.log('Processing attachment upload...');
logger.debug('Processing attachment upload');
try {
const { filename, filePath, fileType, fileSize } = await uploadMissionAttachment(
userId,
missionId,
file
);
console.log('Attachment uploaded successfully to path:', filePath);
logger.debug('Attachment uploaded successfully', { filePath });
// Generate public URL
const publicUrl = getPublicUrl(filePath, S3_CONFIG.bucket);
console.log('Public URL for attachment:', publicUrl);
// Create attachment record in database
console.log('Creating attachment record in database...');
const attachment = await prisma.attachment.create({
data: {
filename,
@ -199,7 +194,7 @@ export async function POST(request: Request) {
uploaderId: userId
}
});
console.log('Attachment record created:', attachment.id);
logger.debug('Attachment record created', { attachmentId: attachment.id });
return NextResponse.json({
success: true,
@ -214,7 +209,10 @@ export async function POST(request: Request) {
}
});
} catch (attachmentError) {
console.error('Error in attachment upload process:', attachmentError);
logger.error('Error in attachment upload process', {
error: attachmentError instanceof Error ? attachmentError.message : String(attachmentError),
missionId
});
return NextResponse.json({
error: 'Attachment upload failed',
details: attachmentError instanceof Error ? attachmentError.message : String(attachmentError)
@ -222,11 +220,13 @@ export async function POST(request: Request) {
}
}
else {
console.log('Invalid upload type:', type);
logger.error('Invalid upload type', { type });
return NextResponse.json({ error: 'Invalid upload type' }, { status: 400 });
}
} catch (error) {
console.error('Unhandled error in upload process:', error);
logger.error('Unhandled error in upload process', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json({
error: 'Internal server error',
details: error instanceof Error ? error.message : String(error)

View File

@ -1,6 +1,7 @@
"use client";
import React, { useState, useEffect } from "react";
import { logger } from '@/lib/logger';
import {
Tabs,
TabsContent,
@ -115,7 +116,7 @@ export function MissionsAdminPanel() {
try {
await Promise.all([fetchUsers(), fetchGroups()]);
} catch (error) {
console.error("Error fetching data:", error);
logger.error("Error fetching data", { error: error instanceof Error ? error.message : String(error) });
} finally {
setLoading(false);
}
@ -134,7 +135,7 @@ export function MissionsAdminPanel() {
const data = await response.json();
setUsers(data);
} catch (error) {
console.error("Error fetching users:", error);
logger.error("Error fetching users", { error: error instanceof Error ? error.message : String(error) });
toast({
title: "Erreur",
description: "Erreur lors de la récupération des utilisateurs",
@ -166,7 +167,7 @@ export function MissionsAdminPanel() {
}
return {...group, membersCount: 0};
} catch (error) {
console.error(`Error fetching members for group ${group.id}:`, error);
logger.error(`Error fetching members for group ${group.id}`, { error: error instanceof Error ? error.message : String(error) });
return {...group, membersCount: 0};
}
})
@ -174,7 +175,7 @@ export function MissionsAdminPanel() {
setGroups(groupsWithCounts);
} catch (error) {
console.error("Error fetching groups:", error);
logger.error("Error fetching groups", { error: error instanceof Error ? error.message : String(error) });
toast({
title: "Erreur",
description: "Erreur lors de la récupération des groupes",
@ -292,7 +293,7 @@ export function MissionsAdminPanel() {
const data = await response.json();
return data;
} catch (error) {
console.error(`Error fetching members for group ${groupId}:`, error);
logger.error(`Error fetching members for group ${groupId}`, { error: error instanceof Error ? error.message : String(error) });
toast({
title: "Erreur",
description: "Erreur lors de la récupération des membres du groupe",
@ -325,7 +326,7 @@ export function MissionsAdminPanel() {
});
}
} catch (error) {
console.error("Error handling group members:", error);
logger.error("Error handling group members", { error: error instanceof Error ? error.message : String(error) });
toast({
title: "Erreur",
description: "Erreur lors de l'affichage des membres du groupe",
@ -421,8 +422,6 @@ export function MissionsAdminPanel() {
logo: missionData.logo // Ensure logo data is included
};
console.log('Submitting mission data:', JSON.stringify(missionSubmitData, null, 2));
// Send to API
const response = await fetch('/api/missions', {
method: 'POST',
@ -448,7 +447,6 @@ export function MissionsAdminPanel() {
router.push('/missions');
} catch (error) {
console.error('Error creating mission:', error);
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Une erreur est survenue lors de la création de la mission",
@ -498,7 +496,6 @@ export function MissionsAdminPanel() {
type="logo"
isNewMission={true}
onFileSelect={(fileData) => {
console.log('Logo file selected:', fileData);
setMissionData(prev => ({
...prev,
logo: fileData

View File

@ -3,6 +3,7 @@
*/
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { logger } from '@/lib/logger';
// Initialize S3 client for Minio
const s3Client = new S3Client({
@ -45,10 +46,8 @@ export async function deleteMissionLogo(missionId: string, logoPath: string): Pr
const normalizedPath = ensureMissionsPrefix(logoPath);
const minioPath = normalizedPath.replace(/^missions\//, ''); // Remove prefix for Minio
console.log('Deleting mission logo:', {
logger.debug('Deleting mission logo', {
missionId,
originalPath: logoPath,
normalizedPath,
minioPath
});
@ -59,14 +58,12 @@ export async function deleteMissionLogo(missionId: string, logoPath: string): Pr
await s3Client.send(command);
console.log('Mission logo deleted successfully:', minioPath);
logger.debug('Mission logo deleted successfully', { minioPath });
} catch (error) {
console.error('Error deleting mission logo:', {
error,
logger.error('Error deleting mission logo', {
error: error instanceof Error ? error.message : String(error),
missionId,
logoPath,
errorType: error instanceof Error ? error.constructor.name : typeof error,
message: error instanceof Error ? error.message : String(error)
minioPath
});
throw error;
}
@ -79,9 +76,7 @@ export async function deleteMissionAttachment(filePath: string): Promise<void> {
const normalizedPath = ensureMissionsPrefix(filePath);
const minioPath = normalizedPath.replace(/^missions\//, ''); // Remove prefix for Minio
console.log('Deleting mission attachment:', {
originalPath: filePath,
normalizedPath,
logger.debug('Deleting mission attachment', {
minioPath
});
@ -92,13 +87,11 @@ export async function deleteMissionAttachment(filePath: string): Promise<void> {
await s3Client.send(command);
console.log('Mission attachment deleted successfully:', minioPath);
logger.debug('Mission attachment deleted successfully', { minioPath });
} catch (error) {
console.error('Error deleting mission attachment:', {
error,
filePath,
errorType: error instanceof Error ? error.constructor.name : typeof error,
message: error instanceof Error ? error.message : String(error)
logger.error('Error deleting mission attachment', {
error: error instanceof Error ? error.message : String(error),
minioPath
});
throw error;
}
@ -107,8 +100,7 @@ export async function deleteMissionAttachment(filePath: string): Promise<void> {
// Upload a mission logo to Minio
export async function uploadMissionLogo(userId: string, missionId: string, file: File): Promise<{ filePath: string }> {
try {
console.log('Starting logo upload:', {
userId,
logger.debug('Starting logo upload', {
missionId,
fileName: file.name,
fileSize: file.size,
@ -123,7 +115,7 @@ export async function uploadMissionLogo(userId: string, missionId: string, file:
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
console.log('Uploading to Minio:', {
logger.debug('Uploading to Minio', {
bucket: 'missions',
key: minioPath,
contentType: file.type
@ -137,20 +129,17 @@ export async function uploadMissionLogo(userId: string, missionId: string, file:
ACL: 'public-read'
}));
console.log('Logo upload successful:', {
logger.debug('Logo upload successful', {
filePath,
minioPath
});
return { filePath };
} catch (error) {
console.error('Error uploading mission logo:', {
error,
userId,
logger.error('Error uploading mission logo', {
error: error instanceof Error ? error.message : String(error),
missionId,
fileName: file.name,
errorType: error instanceof Error ? error.constructor.name : typeof error,
message: error instanceof Error ? error.message : String(error)
fileName: file.name
});
throw error;
}
@ -168,8 +157,7 @@ export async function uploadMissionAttachment(
fileSize: number;
}> {
try {
console.log('Starting attachment upload:', {
userId,
logger.debug('Starting attachment upload', {
missionId,
fileName: file.name,
fileSize: file.size,
@ -183,7 +171,7 @@ export async function uploadMissionAttachment(
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
console.log('Uploading to Minio:', {
logger.debug('Uploading to Minio', {
bucket: 'missions',
key: minioPath,
contentType: file.type
@ -197,7 +185,7 @@ export async function uploadMissionAttachment(
ACL: 'public-read'
}));
console.log('Attachment upload successful:', {
logger.debug('Attachment upload successful', {
filePath,
minioPath
});
@ -209,13 +197,10 @@ export async function uploadMissionAttachment(
fileSize: file.size
};
} catch (error) {
console.error('Error uploading mission attachment:', {
error,
userId,
logger.error('Error uploading mission attachment', {
error: error instanceof Error ? error.message : String(error),
missionId,
fileName: file.name,
errorType: error instanceof Error ? error.constructor.name : typeof error,
message: error instanceof Error ? error.message : String(error)
fileName: file.name
});
throw error;
}

View File

@ -1,4 +1,5 @@
import { env } from '@/lib/env';
import { logger } from '@/lib/logger';
export class N8nService {
private webhookUrl: string;
@ -12,7 +13,7 @@ export class N8nService {
this.apiKey = process.env.N8N_API_KEY || '';
if (!this.apiKey) {
console.error('N8N_API_KEY is not set in environment variables');
logger.error('N8N_API_KEY is not set in environment variables');
}
}
@ -20,9 +21,13 @@ export class N8nService {
try {
const deleteWebhookUrl = process.env.N8N_DELETE_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook-test/mission-delete';
console.log('Triggering n8n mission deletion workflow with data:', JSON.stringify(data, null, 2));
console.log('Using deletion webhook URL:', deleteWebhookUrl);
console.log('API key present:', !!this.apiKey);
logger.debug('Triggering n8n mission deletion workflow', {
missionId: data.missionId,
name: data.name,
hasRepoName: !!data.repoName
});
logger.debug('Using deletion webhook URL', { url: deleteWebhookUrl });
logger.debug('API key present', { present: !!this.apiKey });
const response = await fetch(deleteWebhookUrl, {
method: 'POST',
@ -33,29 +38,39 @@ export class N8nService {
body: JSON.stringify(data),
});
console.log('Deletion webhook response status:', response.status);
console.log('Deletion webhook response headers:', Object.fromEntries(response.headers.entries()));
logger.debug('Deletion webhook response', { status: response.status });
if (!response.ok) {
const errorText = await response.text();
console.error('Deletion webhook error response:', errorText);
logger.error('Deletion webhook error response', {
status: response.status,
error: errorText.substring(0, 200) // Truncate to avoid logging huge responses
});
// Try to parse the error response as JSON for more details
try {
const errorJson = JSON.parse(errorText);
console.error('Parsed error response:', errorJson);
logger.error('Parsed error response', {
code: errorJson.code,
message: errorJson.message
});
} catch (e) {
console.error('Error response is not JSON');
logger.error('Error response is not JSON');
}
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
const responseText = await response.text();
console.log('N8nService - Deletion raw response:', responseText);
logger.debug('N8nService - Deletion raw response received', {
length: responseText.length
});
// Try to parse the response as JSON
try {
const result = JSON.parse(responseText);
console.log('Parsed deletion workflow result:', JSON.stringify(result, null, 2));
logger.debug('Parsed deletion workflow result', {
success: result.success || !result.error,
hasError: !!result.error
});
// Check if the response contains error information
if (result.error || result.message?.includes('failed')) {
@ -70,14 +85,16 @@ export class N8nService {
results: result
};
} catch (parseError) {
console.log('Response is not JSON, treating as workflow trigger confirmation');
logger.debug('Response is not JSON, treating as workflow trigger confirmation');
return {
success: true,
results: { confirmed: true }
};
}
} catch (error) {
console.error('Error triggering n8n deletion workflow:', error);
logger.error('Error triggering n8n deletion workflow', {
error: error instanceof Error ? error.message : String(error)
});
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
@ -87,12 +104,11 @@ export class N8nService {
async triggerMissionCreation(data: any): Promise<any> {
try {
console.log('N8nService - Input data:', {
logger.debug('N8nService - Input data', {
hasServices: Array.isArray(data.services),
services: data.services,
hasGite: data.services?.includes('Gite'),
missionProcessed: data.missionProcessed,
config: data.config
missionId: data.missionId
});
// Clean and validate the data
@ -122,12 +138,11 @@ export class N8nService {
};
// Log the cleaned data
console.log('Sending cleaned data to n8n:', {
logger.debug('Sending cleaned data to n8n', {
name: cleanData.name,
creatorId: cleanData.creatorId,
missionId: cleanData.missionId,
oddScope: cleanData.oddScope,
niveau: cleanData.niveau,
intention: cleanData.intention?.substring(0, 100) + '...', // Log first 100 chars
missionType: cleanData.missionType,
donneurDOrdre: cleanData.donneurDOrdre,
projection: cleanData.projection,
@ -136,12 +151,11 @@ export class N8nService {
profils: cleanData.profils,
hasGuardians: !!cleanData.guardians,
volunteersCount: cleanData.volunteers.length,
hasConfig: !!cleanData.config,
configKeys: cleanData.config ? Object.keys(cleanData.config) : []
hasLogo: !!cleanData.logoPath
});
console.log('Using webhook URL:', this.webhookUrl);
console.log('API key present:', !!this.apiKey);
logger.debug('Using webhook URL', { url: this.webhookUrl });
logger.debug('API key present', { present: !!this.apiKey });
const response = await fetch(this.webhookUrl, {
method: 'POST',
@ -152,29 +166,39 @@ export class N8nService {
body: JSON.stringify(cleanData),
});
console.log('Webhook response status:', response.status);
console.log('Webhook response headers:', Object.fromEntries(response.headers.entries()));
logger.debug('Webhook response', { status: response.status });
if (!response.ok) {
const errorText = await response.text();
console.error('Webhook error response:', errorText);
logger.error('Webhook error response', {
status: response.status,
error: errorText.substring(0, 200) // Truncate to avoid logging huge responses
});
// Try to parse the error response as JSON for more details
try {
const errorJson = JSON.parse(errorText);
console.error('Parsed error response:', errorJson);
logger.error('Parsed error response', {
code: errorJson.code,
message: errorJson.message
});
} catch (e) {
console.error('Error response is not JSON');
logger.error('Error response is not JSON');
}
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
const responseText = await response.text();
console.log('N8nService - Raw response:', responseText);
logger.debug('N8nService - Raw response received', {
length: responseText.length
});
// Try to parse the response as JSON
try {
const result = JSON.parse(responseText);
console.log('Parsed workflow result:', JSON.stringify(result, null, 2));
logger.debug('Parsed workflow result', {
success: !result.error && !result.message?.includes('failed'),
hasError: !!result.error
});
// Check if the response contains error information
if (result.error || result.message?.includes('failed')) {
@ -202,7 +226,7 @@ export class N8nService {
results: result
};
} catch (parseError) {
console.log('Response is not JSON, treating as workflow trigger confirmation');
logger.debug('Response is not JSON, treating as workflow trigger confirmation');
return {
success: true,
results: {
@ -215,7 +239,9 @@ export class N8nService {
};
}
} catch (error) {
console.error('Error triggering n8n workflow:', error);
logger.error('Error triggering n8n workflow', {
error: error instanceof Error ? error.message : String(error)
});
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
@ -225,9 +251,12 @@ export class N8nService {
async triggerMissionRollback(data: any): Promise<any> {
try {
console.log('Triggering n8n rollback workflow with data:', JSON.stringify(data, null, 2));
console.log('Using rollback webhook URL:', this.rollbackWebhookUrl);
console.log('API key present:', !!this.apiKey);
logger.debug('Triggering n8n rollback workflow', {
missionId: data.missionId,
name: data.name
});
logger.debug('Using rollback webhook URL', { url: this.rollbackWebhookUrl });
logger.debug('API key present', { present: !!this.apiKey });
const response = await fetch(this.rollbackWebhookUrl, {
method: 'POST',
@ -240,19 +269,27 @@ export class N8nService {
if (!response.ok) {
const errorText = await response.text();
console.error('Rollback webhook error response:', errorText);
logger.error('Rollback webhook error response', {
status: response.status,
error: errorText.substring(0, 200)
});
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
const result = await response.json();
console.log('Received response from n8n rollback:', JSON.stringify(result, null, 2));
logger.debug('Received response from n8n rollback', {
success: !result.error,
hasError: !!result.error
});
return {
success: true,
results: result
};
} catch (error) {
console.error('Error triggering n8n rollback workflow:', error);
logger.error('Error triggering n8n rollback workflow', {
error: error instanceof Error ? error.message : String(error)
});
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'