From 509f9ea0aa35a6a26170426a012ae94c009bf81e Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 4 May 2025 11:22:35 +0200 Subject: [PATCH] notifications --- .../notifications/leantime-adapter.ts | 376 ++++++++++++------ 1 file changed, 256 insertions(+), 120 deletions(-) diff --git a/lib/services/notifications/leantime-adapter.ts b/lib/services/notifications/leantime-adapter.ts index 676342a9..ac037614 100644 --- a/lib/services/notifications/leantime-adapter.ts +++ b/lib/services/notifications/leantime-adapter.ts @@ -1,6 +1,5 @@ import { Notification, NotificationCount } from '@/lib/types/notification'; import { NotificationAdapter } from './notification-adapter.interface'; -import { prisma } from '@/lib/prisma'; // Leantime notification type from their API interface LeantimeNotification { @@ -18,53 +17,50 @@ interface LeantimeNotification { export class LeantimeAdapter implements NotificationAdapter { readonly sourceName = 'leantime'; private apiUrl: string; - private apiKey: string; + private apiToken: string; constructor() { - // Load from environment or database config + // Load from environment variables, matching the pattern used in other Leantime integrations this.apiUrl = process.env.LEANTIME_API_URL || ''; - this.apiKey = process.env.LEANTIME_API_KEY || ''; - } - - private async getLeantimeCredentials(userId: string): Promise<{ url: string, apiKey: string } | null> { - // Get Leantime credentials from the database for this user - const credentials = await prisma.userServiceCredentials.findFirst({ - where: { - userId: userId, - service: 'leantime' - } - }); - - if (!credentials) { - return null; - } - - return { - url: credentials.serviceUrl || this.apiUrl, - apiKey: credentials.accessToken || this.apiKey, - }; + this.apiToken = process.env.LEANTIME_TOKEN || ''; } async getNotifications(userId: string, page = 1, limit = 20): Promise { - const credentials = await this.getLeantimeCredentials(userId); - if (!credentials) { - return []; - } - try { + // First get the Leantime user ID from email + const email = await this.getUserEmail(userId); + if (!email) { + console.error('Could not get user email for userId:', userId); + return []; + } + + const leantimeUserId = await this.getLeantimeUserId(email); + if (!leantimeUserId) { + console.error('User not found in Leantime:', email); + return []; + } + // Calculate offset for pagination const offset = (page - 1) * limit; - // Make request to Leantime API - const response = await fetch( - `${credentials.url}/api/notifications?limit=${limit}&offset=${offset}`, - { - headers: { - 'Authorization': `Bearer ${credentials.apiKey}`, - 'Content-Type': 'application/json' - } - } - ); + // Make request to Leantime API using jsonrpc + const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiToken + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.notifications.getAll', + params: { + userId: leantimeUserId, + limit: limit, + offset: offset + }, + id: 1 + }) + }); if (!response.ok) { console.error(`Failed to fetch Leantime notifications: ${response.status}`); @@ -72,7 +68,12 @@ export class LeantimeAdapter implements NotificationAdapter { } const data = await response.json(); - return this.transformNotifications(data, userId); + if (!data.result || !Array.isArray(data.result)) { + console.error('Invalid response format from Leantime notifications API'); + return []; + } + + return this.transformNotifications(data.result, userId); } catch (error) { console.error('Error fetching Leantime notifications:', error); return []; @@ -80,49 +81,50 @@ export class LeantimeAdapter implements NotificationAdapter { } async getNotificationCount(userId: string): Promise { - const credentials = await this.getLeantimeCredentials(userId); - if (!credentials) { - return { - total: 0, - unread: 0, - sources: { - leantime: { - total: 0, - unread: 0 - } - } - }; - } - try { - // Make request to Leantime API - const response = await fetch( - `${credentials.url}/api/notifications/count`, - { - headers: { - 'Authorization': `Bearer ${credentials.apiKey}`, - 'Content-Type': 'application/json' - } - } - ); + // First get the Leantime user ID from email + const email = await this.getUserEmail(userId); + if (!email) { + console.error('Could not get user email for userId:', userId); + return this.getEmptyCount(); + } + + const leantimeUserId = await this.getLeantimeUserId(email); + if (!leantimeUserId) { + console.error('User not found in Leantime:', email); + return this.getEmptyCount(); + } + + // Make request to Leantime API using jsonrpc + const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiToken + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.notifications.getNotificationCount', + params: { + userId: leantimeUserId + }, + id: 1 + }) + }); if (!response.ok) { console.error(`Failed to fetch Leantime notification count: ${response.status}`); - return { - total: 0, - unread: 0, - sources: { - leantime: { - total: 0, - unread: 0 - } - } - }; + return this.getEmptyCount(); } const data = await response.json(); - const unreadCount = data.unread || 0; - const totalCount = data.total || 0; + if (!data.result) { + console.error('Invalid response format from Leantime notification count API'); + return this.getEmptyCount(); + } + + const unreadCount = data.result.unread || 0; + const totalCount = data.result.total || 0; return { total: totalCount, @@ -136,42 +138,39 @@ export class LeantimeAdapter implements NotificationAdapter { }; } catch (error) { console.error('Error fetching Leantime notification count:', error); - return { - total: 0, - unread: 0, - sources: { - leantime: { - total: 0, - unread: 0 - } - } - }; + return this.getEmptyCount(); } } async markAsRead(userId: string, notificationId: string): Promise { - const credentials = await this.getLeantimeCredentials(userId); - if (!credentials) { - return false; - } - try { // Extract the source ID from our compound ID const sourceId = notificationId.replace(`${this.sourceName}-`, ''); - // Make request to Leantime API - const response = await fetch( - `${credentials.url}/api/notifications/${sourceId}/read`, - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${credentials.apiKey}`, - 'Content-Type': 'application/json' - } - } - ); + // Make request to Leantime API using jsonrpc + const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiToken + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.notifications.markAsRead', + params: { + notificationId: sourceId + }, + id: 1 + }) + }); - return response.ok; + if (!response.ok) { + console.error(`Failed to mark Leantime notification as read: ${response.status}`); + return false; + } + + const data = await response.json(); + return data.result === true; } catch (error) { console.error('Error marking Leantime notification as read:', error); return false; @@ -179,25 +178,44 @@ export class LeantimeAdapter implements NotificationAdapter { } async markAllAsRead(userId: string): Promise { - const credentials = await this.getLeantimeCredentials(userId); - if (!credentials) { - return false; - } - try { - // Make request to Leantime API - const response = await fetch( - `${credentials.url}/api/notifications/read-all`, - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${credentials.apiKey}`, - 'Content-Type': 'application/json' - } - } - ); + // First get the Leantime user ID from email + const email = await this.getUserEmail(userId); + if (!email) { + console.error('Could not get user email for userId:', userId); + return false; + } + + const leantimeUserId = await this.getLeantimeUserId(email); + if (!leantimeUserId) { + console.error('User not found in Leantime:', email); + return false; + } + + // Make request to Leantime API using jsonrpc + const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiToken + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.notifications.markAllAsRead', + params: { + userId: leantimeUserId + }, + id: 1 + }) + }); - return response.ok; + if (!response.ok) { + console.error(`Failed to mark all Leantime notifications as read: ${response.status}`); + return false; + } + + const data = await response.json(); + return data.result === true; } catch (error) { console.error('Error marking all Leantime notifications as read:', error); return false; @@ -205,7 +223,20 @@ export class LeantimeAdapter implements NotificationAdapter { } async isConfigured(): Promise { - return !!(this.apiUrl && this.apiKey); + return !!(this.apiUrl && this.apiToken); + } + + private getEmptyCount(): NotificationCount { + return { + total: 0, + unread: 0, + sources: { + leantime: { + total: 0, + unread: 0 + } + } + }; } private transformNotifications(data: LeantimeNotification[], userId: string): Notification[] { @@ -233,4 +264,109 @@ export class LeantimeAdapter implements NotificationAdapter { } })); } + + // Helper function to get user's email from ID (using existing method from your system) + private async getUserEmail(userId: string): Promise { + try { + // Fetch from Keycloak similar to how other components do + const response = await fetch(`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}`, { + headers: { + 'Authorization': `Bearer ${await this.getAdminToken()}` + } + }); + + if (!response.ok) { + console.error('Failed to get user from Keycloak'); + return null; + } + + const userData = await response.json(); + return userData.email || null; + } catch (error) { + console.error('Error getting user email:', error); + return null; + } + } + + // Helper function to get Leantime user ID by email (using similar pattern to the tasks API) + private async getLeantimeUserId(email: string): Promise { + try { + if (!this.apiToken) { + console.error('LEANTIME_TOKEN is not set in environment variables'); + return null; + } + + console.log('Fetching Leantime user for email:', email); + + const response = await fetch(`${this.apiUrl}/api/jsonrpc`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': this.apiToken + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.users.getAll', + id: 1 + }), + }); + + if (!response.ok) { + console.error('Failed to fetch Leantime users'); + return null; + } + + const data = await response.json(); + + if (!data.result || !Array.isArray(data.result)) { + console.error('Invalid response format from Leantime users API'); + return null; + } + + const users = data.result; + const user = users.find((u: any) => u.username === email); + + if (user) { + console.log('Found Leantime user:', { id: user.id, username: user.username }); + return user.id; + } else { + console.log('No Leantime user found for email:', email); + return null; + } + } catch (error) { + console.error('Error getting Leantime user ID:', error); + return null; + } + } + + // Helper function to get admin token (similar to the user management API) + private async getAdminToken(): Promise { + try { + const response = await fetch( + `${process.env.KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: process.env.KEYCLOAK_ADMIN_CLIENT_ID || '', + client_secret: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET || '', + }), + } + ); + + if (!response.ok) { + console.error('Failed to get admin token'); + return null; + } + + const tokenData = await response.json(); + return tokenData.access_token; + } catch (error) { + console.error('Error getting admin token:', error); + return null; + } + } } \ No newline at end of file