diff --git a/app/api/missions/route.ts b/app/api/missions/route.ts index 31094c36..66c100e2 100644 --- a/app/api/missions/route.ts +++ b/app/api/missions/route.ts @@ -4,7 +4,7 @@ import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; import { getPublicUrl } from '@/lib/s3'; import { S3_CONFIG } from '@/lib/s3'; -import { IntegrationService } from '@/lib/services/integration-service-fixed'; +import { IntegrationService } from '@/lib/services/integration-service'; // Helper function to check authentication async function checkAuth(request: Request) { diff --git a/lib/services/integration-service-fixed.ts b/lib/services/integration-service-fixed.ts deleted file mode 100644 index c3ddd309..00000000 --- a/lib/services/integration-service-fixed.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { prisma } from '@/lib/prisma'; -import { LeantimeService } from './leantime-service'; -import { OutlineService } from './outline-service-fixed'; -import { RocketChatService } from './rocketchat-service-fixed'; -import axios from 'axios'; - -interface IntegrationResult { - success: boolean; - error?: string; - data?: { - leantimeProjectId?: number; - outlineCollectionId?: string; - rocketChatChannelId?: string; - giteaRepositoryUrl?: string; - penpotProjectId?: string; - }; -} - -export class IntegrationService { - private leantimeService: LeantimeService; - private outlineService: OutlineService; - private rocketChatService: RocketChatService; - - constructor() { - this.leantimeService = new LeantimeService(); - this.outlineService = new OutlineService(); - this.rocketChatService = new RocketChatService(); - } - - /** - * Set up all integrations for a mission - * @param missionId The mission ID - * @returns Integration result - */ - async setupIntegrationsForMission(missionId: string): Promise { - try { - // Get complete mission data with users - const mission = await prisma.mission.findUnique({ - where: { id: missionId }, - include: { - missionUsers: { - include: { - user: true - } - }, - attachments: true - } - }); - - if (!mission) { - throw new Error(`Mission not found: ${missionId}`); - } - - // These fields will store the IDs of created resources - let leantimeProjectId: number | undefined; - let outlineCollectionId: string | undefined; - let rocketChatChannelId: string | undefined; - - // Track which integrations succeeded and which failed - const integrationStatus = { - leantime: { success: false, id: undefined as number | undefined, error: undefined as string | undefined }, - outline: { success: false, id: undefined as string | undefined, error: undefined as string | undefined }, - rocketchat: { success: false, id: undefined as string | undefined, error: undefined as string | undefined } - }; - - // A flag to determine if we should consider this a success or failure overall - let criticalFailure = false; - - try { - // Step 1: Create Leantime project (Consider this a critical integration) - try { - console.log('Starting Leantime project creation...'); - leantimeProjectId = await this.leantimeService.createProject(mission); - console.log(`Leantime project created with ID: ${leantimeProjectId}`); - integrationStatus.leantime.success = true; - integrationStatus.leantime.id = leantimeProjectId; - } catch (leantimeError) { - console.error('Error creating Leantime project:', leantimeError); - integrationStatus.leantime.success = false; - integrationStatus.leantime.error = leantimeError instanceof Error ? leantimeError.message : String(leantimeError); - criticalFailure = true; - throw leantimeError; // Leantime is critical, so we rethrow - } - - // Add a delay to avoid rate limits (extended to 3 seconds) - console.log('Waiting 3 seconds before proceeding to Outline integration...'); - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Step 2: Create Outline collection (Consider this non-critical) - try { - console.log('Starting Outline collection creation...'); - outlineCollectionId = await this.outlineService.createCollection(mission); - console.log(`Outline collection created with ID: ${outlineCollectionId}`); - integrationStatus.outline.success = true; - integrationStatus.outline.id = outlineCollectionId; - } catch (outlineError) { - console.error('Error creating Outline collection:', outlineError); - integrationStatus.outline.success = false; - integrationStatus.outline.error = outlineError instanceof Error ? outlineError.message : String(outlineError); - - // Check if it's an authentication error (401) - if (axios.isAxiosError(outlineError) && outlineError.response?.status === 401) { - console.log('⚠️ Outline authentication error. Please check your API credentials.'); - } else if (axios.isAxiosError(outlineError) && outlineError.response?.status === 429) { - console.log('⚠️ Outline rate limiting error (429). The integration will be skipped for now.'); - } - - // Don't set criticalFailure - Outline is non-critical - } - - // Add a delay to avoid rate limits (extended to 3 seconds) - console.log('Waiting 3 seconds before proceeding to Rocket.Chat integration...'); - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Step 3: Create Rocket.Chat channel (Consider this non-critical) - try { - console.log('Starting Rocket.Chat channel creation...'); - rocketChatChannelId = await this.rocketChatService.createChannel(mission); - console.log(`Rocket.Chat channel created with ID: ${rocketChatChannelId}`); - integrationStatus.rocketchat.success = true; - integrationStatus.rocketchat.id = rocketChatChannelId; - } catch (rocketChatError) { - console.error('Error creating Rocket.Chat channel:', rocketChatError); - integrationStatus.rocketchat.success = false; - integrationStatus.rocketchat.error = rocketChatError instanceof Error ? rocketChatError.message : String(rocketChatError); - - // Check for rate limiting - if (axios.isAxiosError(rocketChatError) && rocketChatError.response?.status === 429) { - console.log('⚠️ Rocket.Chat rate limiting error (429). The integration will be skipped for now.'); - } - // Don't set criticalFailure - Rocket.Chat is non-critical - } - - // Update the mission with the integration IDs (only for successful integrations) - try { - // Use raw SQL or updateMany if the Prisma client doesn't recognize the fields - await prisma.$executeRaw` - UPDATE "Mission" - SET "leantimeProjectId" = ${integrationStatus.leantime.success ? leantimeProjectId?.toString() : null}, - "outlineCollectionId" = ${integrationStatus.outline.success ? outlineCollectionId : null}, - "rocketChatChannelId" = ${integrationStatus.rocketchat.success ? rocketChatChannelId : null} - WHERE "id" = ${missionId} - `; - - console.log('Successfully updated mission with integration IDs'); - } catch (updateError) { - console.error('Error updating mission with integration IDs:', updateError); - // This isn't critical, so don't fail the overall process - } - - // Output a summary of integration results - console.log('Integration results:', JSON.stringify(integrationStatus, null, 2)); - - // If we get here without a critical failure, we consider it a success - // even if some non-critical integrations failed - return { - success: !criticalFailure, - data: { - leantimeProjectId, - outlineCollectionId, - rocketChatChannelId - } - }; - } catch (error) { - console.error('Error setting up integrations:', error); - - // Rollback any created resources - await this.rollbackIntegrations(leantimeProjectId, outlineCollectionId, rocketChatChannelId); - - // Delete the mission itself since a critical integration failed - try { - await prisma.mission.delete({ - where: { id: missionId } - }); - console.log(`Mission ${missionId} deleted due to critical integration failure.`); - } catch (deleteError) { - console.error('Error deleting mission after integration failure:', deleteError); - } - - return { - success: false, - error: `Integration failed: ${error instanceof Error ? error.message : String(error)}` - }; - } - } catch (error) { - console.error('Error in integration setup:', error); - return { - success: false, - error: `Integration setup failed: ${error instanceof Error ? error.message : String(error)}` - }; - } - } - - /** - * Rollback integrations if any step fails - * @param leantimeProjectId The Leantime project ID to delete - * @param outlineCollectionId The Outline collection ID to delete - * @param rocketChatChannelId The Rocket.Chat channel ID to delete - */ - private async rollbackIntegrations( - leantimeProjectId?: number, - outlineCollectionId?: string, - rocketChatChannelId?: string - ): Promise { - console.log('⚠️ Rolling back integrations due to an error...'); - - // Track what we've successfully rolled back - const rollbackStatuses = { - leantime: false, - outline: false, - rocketchat: false - }; - - try { - // Attempt to delete Leantime project - if (leantimeProjectId) { - try { - console.log(`Attempting to delete Leantime project: ${leantimeProjectId}`); - const leantimeSuccess = await this.leantimeService.deleteProject(leantimeProjectId); - rollbackStatuses.leantime = leantimeSuccess; - console.log(`Leantime project deletion ${leantimeSuccess ? 'successful' : 'failed'}: ${leantimeProjectId}`); - - // Add a longer delay to avoid rate limiting (3 seconds) - console.log('Waiting 3 seconds before next rollback operation...'); - await new Promise(resolve => setTimeout(resolve, 3000)); - } catch (leantimeError) { - console.error('Error during Leantime rollback:', leantimeError); - console.log(`⚠️ Note: Leantime project ${leantimeProjectId} may need to be deleted manually`); - } - } - - // Attempt to delete Outline collection - if (outlineCollectionId) { - try { - console.log(`Attempting to delete Outline collection: ${outlineCollectionId}`); - const outlineSuccess = await this.outlineService.deleteCollection(outlineCollectionId); - rollbackStatuses.outline = outlineSuccess; - console.log(`Outline collection deletion ${outlineSuccess ? 'successful' : 'failed'}: ${outlineCollectionId}`); - - // Add a longer delay to avoid rate limiting (3 seconds) - console.log('Waiting 3 seconds before next rollback operation...'); - await new Promise(resolve => setTimeout(resolve, 3000)); - } catch (outlineError) { - console.error('Error during Outline rollback:', outlineError); - console.log(`⚠️ Note: Outline collection ${outlineCollectionId} may need to be deleted manually`); - } - } - - // Attempt to delete Rocket.Chat channel - if (rocketChatChannelId) { - try { - console.log(`Attempting to delete Rocket.Chat channel: ${rocketChatChannelId}`); - const rocketChatSuccess = await this.rocketChatService.deleteChannel(rocketChatChannelId); - rollbackStatuses.rocketchat = rocketChatSuccess; - console.log(`Rocket.Chat channel deletion ${rocketChatSuccess ? 'successful' : 'failed'}: ${rocketChatChannelId}`); - } catch (rocketChatError) { - console.error('Error during Rocket.Chat rollback:', rocketChatError); - console.log(`⚠️ Note: Rocket.Chat channel ${rocketChatChannelId} may need to be deleted manually`); - } - } - - // Provide a summary of rollback operations - console.log('Rollback summary:', JSON.stringify(rollbackStatuses)); - - // If any rollbacks failed, provide a note - if (!rollbackStatuses.leantime || !rollbackStatuses.outline || !rollbackStatuses.rocketchat) { - console.log('⚠️ Some resources may need to be deleted manually.'); - } - } catch (error) { - console.error('Error during rollback:', error); - console.log('⚠️ Resources may need to be deleted manually:'); - if (leantimeProjectId) console.log(`- Leantime project: ${leantimeProjectId}`); - if (outlineCollectionId) console.log(`- Outline collection: ${outlineCollectionId}`); - if (rocketChatChannelId) console.log(`- Rocket.Chat channel: ${rocketChatChannelId}`); - } - } -} \ No newline at end of file diff --git a/lib/services/outline-service-fixed.ts b/lib/services/outline-service-fixed.ts deleted file mode 100644 index 6c8b1abc..00000000 --- a/lib/services/outline-service-fixed.ts +++ /dev/null @@ -1,183 +0,0 @@ -import axios from 'axios'; - -export class OutlineService { - private apiUrl: string; - private apiToken: string; - - constructor() { - this.apiUrl = process.env.OUTLINE_API_URL || 'https://app.getoutline.com/api'; - this.apiToken = process.env.OUTLINE_API_KEY || ''; - } - - /** - * Create a new collection in Outline - * @param mission The mission data - * @returns Collection ID or throws error - */ - async createCollection(mission: any): Promise { - try { - // Log the API details for debugging - console.log('Creating Outline collection with token length:', this.apiToken ? this.apiToken.length : 0); - console.log('Outline API URL:', this.apiUrl); - - const response = await axios.post( - `${this.apiUrl}/collections.create`, - { - name: mission.name, - description: mission.intention || '', - permission: 'read', - private: true - }, - { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiToken}` - } - } - ); - - if (!response.data || !response.data.data || !response.data.data.id) { - throw new Error(`Failed to create Outline collection: ${JSON.stringify(response.data)}`); - } - - const collectionId = response.data.data.id; - console.log(`Created Outline collection with ID: ${collectionId}`); - - // Assign users to the collection - if (mission.missionUsers && mission.missionUsers.length > 0) { - await this.assignUsersToCollection(collectionId, mission.missionUsers); - } - - return collectionId; - } catch (error) { - if (axios.isAxiosError(error) && error.response) { - console.error('Outline API Error Details:', { - status: error.response.status, - statusText: error.response.statusText, - data: error.response.data - }); - } - console.error('Error creating Outline collection:', error); - throw new Error(`Outline integration failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Assign mission users to the Outline collection - * @param collectionId The Outline collection ID - * @param missionUsers The mission users with roles - */ - async assignUsersToCollection(collectionId: string, missionUsers: any[]): Promise { - try { - for (const missionUser of missionUsers) { - // Get the user in Outline - const outlineUserId = await this.getUserByEmail(missionUser.user.email); - if (!outlineUserId) { - console.warn(`User not found in Outline: ${missionUser.user.email}`); - continue; - } - - // Determine permission (Gardien de la Mémoire gets admin, others get read) - const permission = missionUser.role === 'gardien-memoire' ? 'admin' : 'read'; - - // Add the user to the collection - await this.addUserToCollection(collectionId, outlineUserId, permission); - } - } catch (error) { - console.error('Error assigning users to collection:', error); - // Continue even if some user assignments fail - } - } - - /** - * Add a user to a collection with the specified permission - * @param collectionId The Outline collection ID - * @param userId The Outline user ID - * @param permission The permission to assign - */ - async addUserToCollection(collectionId: string, userId: string, permission: 'read' | 'admin'): Promise { - try { - const response = await axios.post( - `${this.apiUrl}/collections.add_user`, - { - id: collectionId, - userId: userId, - permission: permission - }, - { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiToken}` - } - } - ); - - if (!response.data || !response.data.success) { - throw new Error(`Failed to add user to collection: ${JSON.stringify(response.data)}`); - } - - console.log(`Added user ${userId} to collection ${collectionId} with permission ${permission}`); - } catch (error) { - console.error(`Error adding user ${userId} to collection ${collectionId}:`, error); - // Don't fail if individual user assignment fails - } - } - - /** - * Get a user ID by email - * @param email The user email to search for - * @returns The user ID or null if not found - */ - async getUserByEmail(email: string): Promise { - try { - const response = await axios.post( - `${this.apiUrl}/users.info`, - { - email: email - }, - { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiToken}` - } - } - ); - - if (!response.data || !response.data.data || !response.data.data.id) { - return null; - } - - return response.data.data.id; - } catch (error) { - console.error('Error getting user by email:', error); - return null; - } - } - - /** - * Delete a collection from Outline - * @param collectionId The Outline collection ID to delete - * @returns True if successful, false otherwise - */ - async deleteCollection(collectionId: string): Promise { - try { - const response = await axios.post( - `${this.apiUrl}/collections.delete`, - { - id: collectionId - }, - { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiToken}` - } - } - ); - - return response.data && response.data.success === true; - } catch (error) { - console.error(`Error deleting Outline collection ${collectionId}:`, error); - return false; - } - } -} \ No newline at end of file diff --git a/lib/services/rocketchat-service-fixed.ts b/lib/services/rocketchat-service-fixed.ts deleted file mode 100644 index a54e27d7..00000000 --- a/lib/services/rocketchat-service-fixed.ts +++ /dev/null @@ -1,221 +0,0 @@ -import axios from 'axios'; - -export class RocketChatService { - private apiUrl: string; - private authToken: string; - private userId: string; - - constructor() { - // Extract the base URL from the iframe URL - const baseUrl = process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0] || ''; - this.apiUrl = baseUrl; - this.authToken = process.env.ROCKET_CHAT_TOKEN || ''; - this.userId = process.env.ROCKET_CHAT_USER_ID || ''; - } - - /** - * Create a new channel in Rocket.Chat - * @param mission The mission data - * @returns Channel ID or throws error - */ - async createChannel(mission: any): Promise { - try { - // First, get all mission users that need to be added to the channel - const missionUserEmails = mission.missionUsers.map((mu: any) => mu.user.email); - const rocketChatUsernames: string[] = []; - - for (const email of missionUserEmails) { - const user = await this.getUserByEmail(email); - if (user) { - rocketChatUsernames.push(user.username); - } else { - console.warn(`User not found in Rocket.Chat: ${email}`); - } - } - - // Sanitize the channel name to comply with Rocket.Chat restrictions - const channelName = this.sanitizeChannelName(mission.name); - - // Create the channel - const response = await axios.post( - `${this.apiUrl}/api/v1/channels.create`, - { - name: channelName, - members: rocketChatUsernames, - readOnly: false - }, - { - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': this.authToken, - 'X-User-Id': this.userId - } - } - ); - - if (!response.data || !response.data.success) { - throw new Error(`Failed to create Rocket.Chat channel: ${JSON.stringify(response.data)}`); - } - - const channelId = response.data.channel._id; - console.log(`Created Rocket.Chat channel with ID: ${channelId}`); - - // Make "Gardien de la Parole" users channel admins - await this.setChannelAdmins(channelId, mission.missionUsers); - - return channelId; - } catch (error) { - console.error('Error creating Rocket.Chat channel:', error); - throw new Error(`Rocket.Chat integration failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Make "Gardien de la Parole" users channel admins - * @param channelId The Rocket.Chat channel ID - * @param missionUsers The mission users with roles - */ - async setChannelAdmins(channelId: string, missionUsers: any[]): Promise { - try { - // Find "Gardien de la Parole" users - const gardienParoleUsers = missionUsers.filter((mu: any) => mu.role === 'gardien-parole'); - - for (const gardienUser of gardienParoleUsers) { - const user = await this.getUserByEmail(gardienUser.user.email); - if (user) { - await this.makeUserChannelOwner(channelId, user._id); - } - } - } catch (error) { - console.error('Error setting channel admins:', error); - // Don't fail if setting admins fails - } - } - - /** - * Make a user a channel owner - * @param channelId The Rocket.Chat channel ID - * @param userId The Rocket.Chat user ID - */ - async makeUserChannelOwner(channelId: string, userId: string): Promise { - try { - const response = await axios.post( - `${this.apiUrl}/api/v1/channels.addOwner`, - { - roomId: channelId, - userId: userId - }, - { - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': this.authToken, - 'X-User-Id': this.userId - } - } - ); - - if (!response.data || !response.data.success) { - throw new Error(`Failed to make user channel owner: ${JSON.stringify(response.data)}`); - } - - console.log(`Made user ${userId} an owner of channel ${channelId}`); - } catch (error) { - console.error(`Error making user ${userId} channel owner:`, error); - // Don't fail if individual user assignment fails - } - } - - /** - * Get a user by email - * @param email The user email to search for - * @returns The user object or null if not found - */ - async getUserByEmail(email: string): Promise { - try { - // First try exact email match - const response = await axios.get( - `${this.apiUrl}/api/v1/users.list`, - { - headers: { - 'X-Auth-Token': this.authToken, - 'X-User-Id': this.userId - } - } - ); - - if (!response.data || !response.data.success) { - throw new Error(`Failed to get users: ${JSON.stringify(response.data)}`); - } - - // First check if user exists by email - const userByEmail = response.data.users.find((user: any) => - user.emails?.some((e: any) => e.address === email) - ); - - if (userByEmail) { - return userByEmail; - } - - // If not found by email, try by username (username might be the part before @) - const username = email.split('@')[0]; - const userByUsername = response.data.users.find((user: any) => user.username === username); - - return userByUsername || null; - } catch (error) { - console.error('Error getting user by email:', error); - return null; - } - } - - /** - * Sanitize a channel name to comply with Rocket.Chat restrictions - * @param name The original name - * @returns The sanitized name - */ - private sanitizeChannelName(name: string): string { - // Replace spaces and invalid characters with hyphens - // Channel names must match regex [0-9a-zA-Z-_.]+ - let sanitized = name.replace(/[^0-9a-zA-Z-_.]/g, '-'); - - // Ensure no sequential hyphens - sanitized = sanitized.replace(/-+/g, '-'); - - // Trim hyphens from beginning and end - sanitized = sanitized.replace(/^-+|-+$/g, ''); - - // Ensure name is not empty after sanitization - if (!sanitized) { - sanitized = 'mission-channel'; - } - - return sanitized.toLowerCase(); - } - - /** - * Delete a channel from Rocket.Chat - * @param channelId The Rocket.Chat channel ID to delete - * @returns True if successful, false otherwise - */ - async deleteChannel(channelId: string): Promise { - try { - const response = await axios.post( - `${this.apiUrl}/api/v1/channels.delete`, - { - roomId: channelId - }, - { - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': this.authToken, - 'X-User-Id': this.userId - } - } - ); - - return response.data && response.data.success === true; - } catch (error) { - console.error(`Error deleting Rocket.Chat channel ${channelId}:`, error); - return false; - } - } -} \ No newline at end of file