From 1bd2673ac95414d3ff5ba935dee7dd4e5c9c82c6 Mon Sep 17 00:00:00 2001 From: alma Date: Tue, 6 May 2025 16:14:09 +0200 Subject: [PATCH] missions api2 --- lib/services/integration-service.ts | 163 ++++++++++++++++++++++------ lib/services/leantime-service.ts | 91 +++++++++------- lib/services/outline-service.ts | 11 ++ 3 files changed, 194 insertions(+), 71 deletions(-) diff --git a/lib/services/integration-service.ts b/lib/services/integration-service.ts index 2f2c551e..c77b55f0 100644 --- a/lib/services/integration-service.ts +++ b/lib/services/integration-service.ts @@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma'; import { LeantimeService } from './leantime-service'; import { OutlineService } from './outline-service'; import { RocketChatService } from './rocketchat-service'; +import axios from 'axios'; interface IntegrationResult { success: boolean; @@ -54,44 +55,88 @@ export class IntegrationService { 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 - leantimeProjectId = await this.leantimeService.createProject(mission); - console.log(`Leantime project created with ID: ${leantimeProjectId}`); - - // Step 2: Create Outline collection - outlineCollectionId = await this.outlineService.createCollection(mission); - console.log(`Outline collection created with ID: ${outlineCollectionId}`); - - // Step 3: Create Rocket.Chat channel - rocketChatChannelId = await this.rocketChatService.createChannel(mission); - console.log(`Rocket.Chat channel created with ID: ${rocketChatChannelId}`); - - // Add integrations for specific services - if (mission.services.includes('gite')) { - // TODO: Add Gitea integration when API docs are available - console.log('Gitea service requested but integration not implemented yet'); + // Step 1: Create Leantime project (Consider this a critical integration) + try { + 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 } - if (mission.services.includes('artlab')) { - // TODO: Add Penpot integration when API docs are available - console.log('Artlab service requested but integration not implemented yet'); + // Add a delay to avoid rate limits + await new Promise(resolve => setTimeout(resolve, 500)); + + // Step 2: Create Outline collection (Consider this non-critical) + try { + 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.'); + } + + // Don't set criticalFailure - Outline is non-critical } - - // Update the mission with the integration IDs + + // Add a delay to avoid rate limits + await new Promise(resolve => setTimeout(resolve, 500)); + + // Step 3: Create Rocket.Chat channel (Consider this non-critical) + try { + 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); + // Don't set criticalFailure - Rocket.Chat is non-critical + } + + // Update the mission with the integration IDs (only for successful integrations) await prisma.mission.update({ where: { id: missionId }, data: { - leantimeProjectId: leantimeProjectId.toString(), - outlineCollectionId: outlineCollectionId, - rocketChatChannelId: rocketChatChannelId, + leantimeProjectId: integrationStatus.leantime.success ? leantimeProjectId?.toString() : undefined, + outlineCollectionId: integrationStatus.outline.success ? outlineCollectionId : undefined, + rocketChatChannelId: integrationStatus.rocketchat.success ? rocketChatChannelId : undefined, // giteaRepositoryUrl and penpotProjectId will be added when implemented } }); + // 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: true, + success: !criticalFailure, data: { leantimeProjectId, outlineCollectionId, @@ -104,10 +149,15 @@ export class IntegrationService { // Rollback any created resources await this.rollbackIntegrations(leantimeProjectId, outlineCollectionId, rocketChatChannelId); - // Delete the mission itself since an integration failed - await prisma.mission.delete({ - where: { id: missionId } - }); + // 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, @@ -134,24 +184,71 @@ export class IntegrationService { 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) { - await this.leantimeService.deleteProject(leantimeProjectId); + try { + const leantimeSuccess = await this.leantimeService.deleteProject(leantimeProjectId); + rollbackStatuses.leantime = leantimeSuccess; + console.log(`Leantime project deletion ${leantimeSuccess ? 'successful' : 'failed'}: ${leantimeProjectId}`); + + // Add a delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 1000)); + } 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) { - await this.outlineService.deleteCollection(outlineCollectionId); + try { + const outlineSuccess = await this.outlineService.deleteCollection(outlineCollectionId); + rollbackStatuses.outline = outlineSuccess; + console.log(`Outline collection deletion ${outlineSuccess ? 'successful' : 'failed'}: ${outlineCollectionId}`); + + // Add a delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 1000)); + } 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) { - await this.rocketChatService.deleteChannel(rocketChatChannelId); + try { + 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); - // Even if rollback fails, we continue with mission deletion + 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/leantime-service.ts b/lib/services/leantime-service.ts index 005845aa..5fe5c5f0 100644 --- a/lib/services/leantime-service.ts +++ b/lib/services/leantime-service.ts @@ -576,51 +576,66 @@ export class LeantimeService { } } catch (directError) { console.log('Direct email lookup failed, trying alternate method...'); + + // Check if the error is rate limiting (429) + if (axios.isAxiosError(directError) && directError.response?.status === 429) { + console.log('Rate limiting detected (429). Waiting before retry...'); + // Wait 1 second before next API call to respect rate limits + await new Promise(resolve => setTimeout(resolve, 1000)); + } } // Fall back to get all users and filter - const response = await axios.post( - this.getApiEndpoint(), - { - method: 'leantime.rpc.Users.Users.getAll', - jsonrpc: '2.0', - id: 1 - }, - { - headers: { - 'Content-Type': 'application/json', - 'X-API-Key': this.apiToken + try { + const response = await axios.post( + this.getApiEndpoint(), + { + method: 'leantime.rpc.Users.Users.getAll', + jsonrpc: '2.0', + id: 1 + }, + { + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiToken + } } - } - ); - - if (!response.data || !response.data.result) { - throw new Error(`Failed to get users: ${JSON.stringify(response.data)}`); - } - - const users = response.data.result; - - // First try exact email match - let user = users.find((u: any) => u.email === email || u.username === email); - - // If that fails, try case-insensitive match - if (!user) { - const lowerEmail = email.toLowerCase(); - user = users.find((u: any) => - (u.email && u.email.toLowerCase() === lowerEmail) || - (u.username && u.username.toLowerCase() === lowerEmail) ); - } - if (user) { - console.log(`Found user with email ${email}: ID ${user.id}`); - return user.id; - } else { - // If user is still not found, we might need to create them - // This would require additional code to create a user if needed - console.log(`No user found with email ${email}`); - return null; + if (!response.data || !response.data.result) { + throw new Error(`Failed to get users: ${JSON.stringify(response.data)}`); + } + + const users = response.data.result; + + // First try exact email match + let user = users.find((u: any) => u.email === email || u.username === email); + + // If that fails, try case-insensitive match + if (!user) { + const lowerEmail = email.toLowerCase(); + user = users.find((u: any) => + (u.email && u.email.toLowerCase() === lowerEmail) || + (u.username && u.username.toLowerCase() === lowerEmail) + ); + } + + if (user) { + console.log(`Found user with email ${email}: ID ${user.id}`); + return user.id; + } + } catch (fetchError) { + // Check if this is a rate limiting error + if (axios.isAxiosError(fetchError) && fetchError.response?.status === 429) { + console.log('Rate limiting detected (429). Consider user assignment through manual UI.'); + } else { + console.error('Error fetching users:', fetchError); + } } + + // If user is still not found or error occurred + console.log(`No user found with email ${email}`); + return null; } catch (error) { console.error('Error getting user by email:', error); return null; diff --git a/lib/services/outline-service.ts b/lib/services/outline-service.ts index 70093db7..6c8b1abc 100644 --- a/lib/services/outline-service.ts +++ b/lib/services/outline-service.ts @@ -16,6 +16,10 @@ export class OutlineService { */ 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`, { @@ -46,6 +50,13 @@ export class OutlineService { 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)}`); }