From 27f5b7dd2fabf438de7d3d9f80efa06a7fd0ddba Mon Sep 17 00:00:00 2001 From: alma Date: Tue, 6 May 2025 15:36:15 +0200 Subject: [PATCH] missions page 2 --- app/api/missions/route.ts | 139 ++++++++----- lib/services/integration-service.ts | 157 +++++++++++++++ lib/services/leantime-service.ts | 291 ++++++++++++++++++++++++++++ lib/services/outline-service.ts | 172 ++++++++++++++++ lib/services/rocketchat-service.ts | 221 +++++++++++++++++++++ prisma/schema.prisma | 7 + 6 files changed, 934 insertions(+), 53 deletions(-) create mode 100644 lib/services/integration-service.ts create mode 100644 lib/services/leantime-service.ts create mode 100644 lib/services/outline-service.ts create mode 100644 lib/services/rocketchat-service.ts diff --git a/app/api/missions/route.ts b/app/api/missions/route.ts index 2124acb0..66c100e2 100644 --- a/app/api/missions/route.ts +++ b/app/api/missions/route.ts @@ -4,6 +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'; // Helper function to check authentication async function checkAuth(request: Request) { @@ -44,7 +45,7 @@ export async function GET(request: Request) { } // Get missions with basic info - const missions = await prisma.mission.findMany({ + const missions = await (prisma as any).mission.findMany({ where, skip: offset, take: limit, @@ -83,10 +84,10 @@ export async function GET(request: Request) { }); // Get total count - const totalCount = await prisma.mission.count({ where }); + const totalCount = await (prisma as any).mission.count({ where }); // Transform logo paths to public URLs - const missionsWithPublicUrls = missions.map(mission => ({ + const missionsWithPublicUrls = missions.map((mission: any) => ({ ...mission, logo: mission.logo ? `/api/missions/image/${mission.logo}` : null })); @@ -157,63 +158,95 @@ export async function POST(request: Request) { }, { status: 400 }); } - // Create the mission - const mission = await prisma.mission.create({ - data: { - name, - logo, - oddScope: oddScope || [], - niveau, - intention, - missionType, - donneurDOrdre, - projection, - services: services || [], - participation, - profils: profils || [], - creatorId: userId + // Wrap the mission creation and integration in a transaction + const result = await prisma.$transaction(async (tx: any) => { + // Create the mission + const mission = await tx.mission.create({ + data: { + name, + logo, + oddScope: oddScope || [], + niveau, + intention, + missionType, + donneurDOrdre, + projection, + services: services || [], + participation, + profils: profils || [], + creatorId: userId + } + }); + + // Add guardians if provided + if (guardians) { + const guardianRoles = ['gardien-temps', 'gardien-parole', 'gardien-memoire']; + const guardianEntries = Object.entries(guardians) + .filter(([role, userId]) => guardianRoles.includes(role) && userId) + .map(([role, userId]) => ({ + role, + userId: userId as string, + missionId: mission.id + })); + + if (guardianEntries.length > 0) { + await tx.missionUser.createMany({ + data: guardianEntries + }); + } } - }); - - // Add guardians if provided - if (guardians) { - const guardianRoles = ['gardien-temps', 'gardien-parole', 'gardien-memoire']; - const guardianEntries = Object.entries(guardians) - .filter(([role, userId]) => guardianRoles.includes(role) && userId) - .map(([role, userId]) => ({ - role, - userId: userId as string, + + // Add volunteers if provided + if (volunteers && volunteers.length > 0) { + const volunteerEntries = volunteers.map((userId: string) => ({ + role: 'volontaire', + userId, missionId: mission.id })); - - if (guardianEntries.length > 0) { - await prisma.missionUser.createMany({ - data: guardianEntries + + await tx.missionUser.createMany({ + data: volunteerEntries }); } - } - - // Add volunteers if provided - if (volunteers && volunteers.length > 0) { - const volunteerEntries = volunteers.map((userId: string) => ({ - role: 'volontaire', - userId, - missionId: mission.id - })); - await prisma.missionUser.createMany({ - data: volunteerEntries - }); - } - - return NextResponse.json({ - success: true, - mission: { - id: mission.id, - name: mission.name, - createdAt: mission.createdAt - } + return mission; }); + + try { + // Initialize external integrations after transaction completes + const integrationService = new IntegrationService(); + const integrationResult = await integrationService.setupIntegrationsForMission(result.id); + + if (!integrationResult.success) { + // If integration failed, the mission was already deleted in the integration service + return NextResponse.json({ + error: 'Failed to set up external services', + details: integrationResult.error + }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + mission: { + id: result.id, + name: result.name, + createdAt: result.createdAt + }, + integrations: { + status: 'success', + data: integrationResult.data + } + }); + } catch (integrationError) { + // If there's any unhandled error, delete the mission and report failure + console.error('Integration error:', integrationError); + await (prisma as any).mission.delete({ where: { id: result.id } }); + + return NextResponse.json({ + error: 'Failed to set up external services', + details: integrationError instanceof Error ? integrationError.message : String(integrationError) + }, { status: 500 }); + } } catch (error) { console.error('Error creating mission:', error); return NextResponse.json({ diff --git a/lib/services/integration-service.ts b/lib/services/integration-service.ts new file mode 100644 index 00000000..2f2c551e --- /dev/null +++ b/lib/services/integration-service.ts @@ -0,0 +1,157 @@ +import { prisma } from '@/lib/prisma'; +import { LeantimeService } from './leantime-service'; +import { OutlineService } from './outline-service'; +import { RocketChatService } from './rocketchat-service'; + +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; + + 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'); + } + + if (mission.services.includes('artlab')) { + // TODO: Add Penpot integration when API docs are available + console.log('Artlab service requested but integration not implemented yet'); + } + + // Update the mission with the integration IDs + await prisma.mission.update({ + where: { id: missionId }, + data: { + leantimeProjectId: leantimeProjectId.toString(), + outlineCollectionId: outlineCollectionId, + rocketChatChannelId: rocketChatChannelId, + // giteaRepositoryUrl and penpotProjectId will be added when implemented + } + }); + + return { + success: true, + 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 an integration failed + await prisma.mission.delete({ + where: { id: missionId } + }); + + 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 { + try { + // Attempt to delete Leantime project + if (leantimeProjectId) { + await this.leantimeService.deleteProject(leantimeProjectId); + } + + // Attempt to delete Outline collection + if (outlineCollectionId) { + await this.outlineService.deleteCollection(outlineCollectionId); + } + + // Attempt to delete Rocket.Chat channel + if (rocketChatChannelId) { + await this.rocketChatService.deleteChannel(rocketChatChannelId); + } + } catch (error) { + console.error('Error during rollback:', error); + // Even if rollback fails, we continue with mission deletion + } + } +} \ No newline at end of file diff --git a/lib/services/leantime-service.ts b/lib/services/leantime-service.ts new file mode 100644 index 00000000..a263c093 --- /dev/null +++ b/lib/services/leantime-service.ts @@ -0,0 +1,291 @@ +import axios from 'axios'; + +export class LeantimeService { + private apiUrl: string; + private apiToken: string; + + constructor() { + this.apiUrl = process.env.LEANTIME_API_URL || ''; + this.apiToken = process.env.LEANTIME_TOKEN || ''; + } + + /** + * Create a new project in Leantime + * @param mission The mission data + * @returns Project ID or throws error + */ + async createProject(mission: any): Promise { + try { + // Determine client ID based on mission type + const clientId = mission.niveau.toLowerCase() === 'a' ? + await this.getClientIdByName('Enkun') : + await this.getClientIdByName('ONG'); + + if (!clientId) { + throw new Error(`Leantime client not found for mission type ${mission.niveau}`); + } + + // Create the project + const response = await axios.post( + this.apiUrl, + { + method: 'leantime.rpc.Projects.Projects.addProject', + jsonrpc: '2.0', + id: 1, + params: { + values: { + name: mission.name, + details: mission.intention || '', + clientId: clientId, + type: 'project', + psettings: 'restricted', + } + } + }, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiToken}` + } + } + ); + + if (!response.data || !response.data.result) { + throw new Error(`Failed to create Leantime project: ${JSON.stringify(response.data)}`); + } + + const projectId = response.data.result; + console.log(`Created Leantime project with ID: ${projectId}`); + + // If the mission has a logo, set it as project avatar + if (mission.logo) { + await this.setProjectAvatar(projectId, mission.logo); + } + + // Assign users to the project + if (mission.missionUsers && mission.missionUsers.length > 0) { + await this.assignUsersToProject(projectId, mission.missionUsers); + } + + return projectId; + } catch (error) { + console.error('Error creating Leantime project:', error); + throw new Error(`Leantime integration failed: ${error instanceof Error ? error.message : String(error)}`); + } + } + + /** + * Set project avatar using mission logo + * @param projectId The Leantime project ID + * @param logoUrl The mission logo URL + */ + async setProjectAvatar(projectId: number, logoPath: string): Promise { + try { + // Get the logo file from the storage + const logoResponse = await fetch(`/api/missions/image/${logoPath}`); + if (!logoResponse.ok) { + throw new Error(`Failed to fetch logo file: ${logoResponse.statusText}`); + } + + const formData = new FormData(); + const logoBlob = await logoResponse.blob(); + + // Add the file to form data + formData.append('file', logoBlob, 'logo.png'); + formData.append('project', JSON.stringify({ id: projectId })); + + // Upload the avatar + const response = await axios.post( + `${this.apiUrl.replace('/api/jsonrpc.php', '')}/api/v1/projects.setProjectAvatar`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + 'Authorization': `Bearer ${this.apiToken}` + } + } + ); + + if (!response.data || !response.data.success) { + throw new Error(`Failed to set project avatar: ${JSON.stringify(response.data)}`); + } + + console.log(`Set avatar for Leantime project ${projectId}`); + } catch (error) { + console.error('Error setting project avatar:', error); + // Don't fail the entire process if avatar upload fails + } + } + + /** + * Assign mission users to the Leantime project + * @param projectId The Leantime project ID + * @param missionUsers The mission users with roles + */ + async assignUsersToProject(projectId: number, missionUsers: any[]): Promise { + try { + for (const missionUser of missionUsers) { + // Get or create the user in Leantime + const leantimeUserId = await this.getUserByEmail(missionUser.user.email); + if (!leantimeUserId) { + console.warn(`User not found in Leantime: ${missionUser.user.email}`); + continue; + } + + // Determine role (Gardien du Temps gets editor, others get commenter) + const role = missionUser.role === 'gardien-temps' ? 'editor' : 'commenter'; + + // Assign the user to the project + await this.assignUserToProject(projectId, leantimeUserId, role); + } + } catch (error) { + console.error('Error assigning users to project:', error); + // Continue even if some user assignments fail + } + } + + /** + * Assign a user to a project with the specified role + * @param projectId The Leantime project ID + * @param userId The Leantime user ID + * @param role The role to assign + */ + async assignUserToProject(projectId: number, userId: string, role: string): Promise { + try { + const response = await axios.post( + this.apiUrl, + { + method: 'leantime.rpc.Projects.Projects.assignUserToProject', + jsonrpc: '2.0', + id: 1, + params: { + projectId, + userId, + role + } + }, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiToken}` + } + } + ); + + if (!response.data || !response.data.result) { + throw new Error(`Failed to assign user to project: ${JSON.stringify(response.data)}`); + } + + console.log(`Assigned user ${userId} to project ${projectId} with role ${role}`); + } catch (error) { + console.error(`Error assigning user ${userId} to project ${projectId}:`, error); + // Don't fail if individual user assignment fails + } + } + + /** + * Get a client ID by name + * @param clientName The client name to search for + * @returns The client ID or null if not found + */ + async getClientIdByName(clientName: string): Promise { + try { + const response = await axios.post( + this.apiUrl, + { + method: 'leantime.rpc.Clients.Clients.getAll', + jsonrpc: '2.0', + id: 1, + }, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiToken}` + } + } + ); + + if (!response.data || !response.data.result) { + throw new Error(`Failed to get clients: ${JSON.stringify(response.data)}`); + } + + const clients = response.data.result; + const client = clients.find((c: any) => + c.name.toLowerCase() === clientName.toLowerCase() + ); + + return client ? parseInt(client.id) : null; + } catch (error) { + console.error('Error getting client by name:', error); + return null; + } + } + + /** + * 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, + { + method: 'leantime.rpc.Users.Users.getAll', + jsonrpc: '2.0', + id: 1, + }, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiToken}` + } + } + ); + + if (!response.data || !response.data.result) { + throw new Error(`Failed to get users: ${JSON.stringify(response.data)}`); + } + + const users = response.data.result; + const user = users.find((u: any) => u.email === email); + + return user ? user.id : null; + } catch (error) { + console.error('Error getting user by email:', error); + return null; + } + } + + /** + * Delete a project from Leantime + * @param projectId The Leantime project ID to delete + * @returns True if successful, false otherwise + */ + async deleteProject(projectId: number): Promise { + try { + const response = await axios.post( + this.apiUrl, + { + method: 'leantime.rpc.Projects.Projects.deleteProject', + jsonrpc: '2.0', + id: 1, + params: { + id: projectId + } + }, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiToken}` + } + } + ); + + return response.data && response.data.result === true; + } catch (error) { + console.error(`Error deleting Leantime project ${projectId}:`, error); + return false; + } + } +} \ No newline at end of file diff --git a/lib/services/outline-service.ts b/lib/services/outline-service.ts new file mode 100644 index 00000000..70093db7 --- /dev/null +++ b/lib/services/outline-service.ts @@ -0,0 +1,172 @@ +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 { + 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) { + 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.ts b/lib/services/rocketchat-service.ts new file mode 100644 index 00000000..dc091635 --- /dev/null +++ b/lib/services/rocketchat-service.ts @@ -0,0 +1,221 @@ +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 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b2f7c45e..15068aaf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -138,6 +138,13 @@ model Mission { attachments Attachment[] missionUsers MissionUser[] + // External integration fields + leantimeProjectId String? + outlineCollectionId String? + rocketChatChannelId String? + giteaRepositoryUrl String? + penpotProjectId String? + @@index([creatorId]) }