454 lines
16 KiB
TypeScript
454 lines
16 KiB
TypeScript
import { Notification, NotificationCount } from '@/lib/types/notification';
|
|
import { NotificationAdapter } from './notification-adapter.interface';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
|
|
|
// Leantime notification type from their API
|
|
interface LeantimeNotification {
|
|
id: number;
|
|
userId: number;
|
|
username: string;
|
|
message: string;
|
|
type: string;
|
|
moduleId: number;
|
|
url: string;
|
|
read: number; // 0 for unread, 1 for read
|
|
date: string; // ISO format date
|
|
}
|
|
|
|
export class LeantimeAdapter implements NotificationAdapter {
|
|
readonly sourceName = 'leantime';
|
|
private apiUrl: string;
|
|
private apiToken: string;
|
|
|
|
constructor() {
|
|
// Load from environment variables, matching the pattern used in other Leantime integrations
|
|
this.apiUrl = process.env.LEANTIME_API_URL || '';
|
|
this.apiToken = process.env.LEANTIME_TOKEN || '';
|
|
|
|
// Log configuration on initialization
|
|
console.log('[LEANTIME_ADAPTER] Initialized with:', {
|
|
apiUrlConfigured: !!this.apiUrl,
|
|
apiTokenConfigured: !!this.apiToken,
|
|
apiUrlPrefix: this.apiUrl ? this.apiUrl.substring(0, 30) + '...' : 'not set',
|
|
});
|
|
}
|
|
|
|
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
|
|
console.log(`[LEANTIME_ADAPTER] getNotifications called for userId: ${userId}, page: ${page}, limit: ${limit}`);
|
|
|
|
try {
|
|
// Get the user's email directly from the session
|
|
const email = await this.getUserEmail();
|
|
console.log(`[LEANTIME_ADAPTER] Retrieved email from session:`, email || 'null');
|
|
|
|
if (!email) {
|
|
console.error('[LEANTIME_ADAPTER] Could not get user email from session');
|
|
return [];
|
|
}
|
|
|
|
const leantimeUserId = await this.getLeantimeUserId(email);
|
|
console.log(`[LEANTIME_ADAPTER] Retrieved Leantime userId for email ${email}:`, leantimeUserId || 'null');
|
|
|
|
if (!leantimeUserId) {
|
|
console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email);
|
|
return [];
|
|
}
|
|
|
|
// Calculate offset for pagination
|
|
const offset = (page - 1) * limit;
|
|
|
|
// Make request to Leantime API using jsonrpc
|
|
console.log('[LEANTIME_ADAPTER] Sending request to Leantime API for notifications');
|
|
const jsonRpcBody = {
|
|
jsonrpc: '2.0',
|
|
method: 'leantime.rpc.notifications.getNotifications',
|
|
params: {
|
|
userId: leantimeUserId,
|
|
limit: limit
|
|
},
|
|
id: 1
|
|
};
|
|
console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody));
|
|
|
|
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': this.apiToken
|
|
},
|
|
body: JSON.stringify(jsonRpcBody)
|
|
});
|
|
|
|
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime notifications:', {
|
|
status: response.status,
|
|
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
|
|
});
|
|
return [];
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
console.log('[LEANTIME_ADAPTER] Raw response:', responseText.substring(0, 200) + (responseText.length > 200 ? '...' : ''));
|
|
|
|
const data = JSON.parse(responseText);
|
|
console.log('[LEANTIME_ADAPTER] Parsed response data:', {
|
|
hasResult: !!data.result,
|
|
resultIsArray: Array.isArray(data.result),
|
|
resultLength: Array.isArray(data.result) ? data.result.length : 'n/a',
|
|
error: data.error
|
|
});
|
|
|
|
if (!data.result || !Array.isArray(data.result)) {
|
|
if (data.error) {
|
|
console.error(`[LEANTIME_ADAPTER] API error: ${data.error.message || JSON.stringify(data.error)}`);
|
|
} else {
|
|
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime notifications API');
|
|
}
|
|
return [];
|
|
}
|
|
|
|
const notifications = this.transformNotifications(data.result, userId);
|
|
console.log('[LEANTIME_ADAPTER] Transformed notifications count:', notifications.length);
|
|
return notifications;
|
|
} catch (error) {
|
|
console.error('[LEANTIME_ADAPTER] Error fetching Leantime notifications:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getNotificationCount(userId: string): Promise<NotificationCount> {
|
|
console.log(`[LEANTIME_ADAPTER] getNotificationCount called for userId: ${userId}`);
|
|
|
|
try {
|
|
// Get the user's email directly from the session
|
|
const email = await this.getUserEmail();
|
|
console.log(`[LEANTIME_ADAPTER] Retrieved email from session:`, email || 'null');
|
|
|
|
if (!email) {
|
|
console.error('[LEANTIME_ADAPTER] Could not get user email from session');
|
|
return this.getEmptyCount();
|
|
}
|
|
|
|
const leantimeUserId = await this.getLeantimeUserId(email);
|
|
console.log(`[LEANTIME_ADAPTER] Retrieved Leantime userId for email ${email}:`, leantimeUserId || 'null');
|
|
|
|
if (!leantimeUserId) {
|
|
console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email);
|
|
return this.getEmptyCount();
|
|
}
|
|
|
|
// Make request to Leantime API using jsonrpc to get all notifications
|
|
console.log('[LEANTIME_ADAPTER] Sending request to Leantime API for notification count');
|
|
const jsonRpcBody = {
|
|
jsonrpc: '2.0',
|
|
method: 'leantime.rpc.notifications.getNotifications',
|
|
params: {
|
|
userId: leantimeUserId
|
|
},
|
|
id: 1
|
|
};
|
|
console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody));
|
|
|
|
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': this.apiToken
|
|
},
|
|
body: JSON.stringify(jsonRpcBody)
|
|
});
|
|
|
|
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime notification count:', {
|
|
status: response.status,
|
|
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
|
|
});
|
|
return this.getEmptyCount();
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
console.log('[LEANTIME_ADAPTER] Raw response:', responseText.substring(0, 200) + (responseText.length > 200 ? '...' : ''));
|
|
|
|
const data = JSON.parse(responseText);
|
|
console.log('[LEANTIME_ADAPTER] Parsed response data:', {
|
|
hasResult: !!data.result,
|
|
resultIsArray: Array.isArray(data.result),
|
|
resultLength: Array.isArray(data.result) ? data.result.length : 'n/a',
|
|
error: data.error
|
|
});
|
|
|
|
if (!data.result || !Array.isArray(data.result)) {
|
|
if (data.error) {
|
|
console.error(`[LEANTIME_ADAPTER] API error: ${data.error.message || JSON.stringify(data.error)}`);
|
|
} else {
|
|
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime notifications API');
|
|
}
|
|
return this.getEmptyCount();
|
|
}
|
|
|
|
// Count total and unread notifications
|
|
const notifications = data.result;
|
|
const totalCount = notifications.length;
|
|
const unreadCount = notifications.filter((n: any) => n.read === 0).length;
|
|
|
|
console.log('[LEANTIME_ADAPTER] Notification counts:', { unread: unreadCount, total: totalCount });
|
|
|
|
return {
|
|
total: totalCount,
|
|
unread: unreadCount,
|
|
sources: {
|
|
leantime: {
|
|
total: totalCount,
|
|
unread: unreadCount
|
|
}
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('[LEANTIME_ADAPTER] Error fetching Leantime notification count:', error);
|
|
return this.getEmptyCount();
|
|
}
|
|
}
|
|
|
|
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
|
|
try {
|
|
// Extract the source ID from our compound ID
|
|
const sourceId = notificationId.replace(`${this.sourceName}-`, '');
|
|
|
|
// 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.markNotificationRead',
|
|
params: {
|
|
id: parseInt(sourceId)
|
|
},
|
|
id: 1
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('[LEANTIME_ADAPTER] Failed to mark Leantime notification as read:', {
|
|
status: response.status,
|
|
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
|
|
});
|
|
return false;
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.error) {
|
|
console.error(`[LEANTIME_ADAPTER] API error: ${data.error.message || JSON.stringify(data.error)}`);
|
|
return false;
|
|
}
|
|
|
|
return data.result === true;
|
|
} catch (error) {
|
|
console.error('[LEANTIME_ADAPTER] Error marking Leantime notification as read:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async markAllAsRead(userId: string): Promise<boolean> {
|
|
try {
|
|
// Get the user's email directly from the session
|
|
const email = await this.getUserEmail();
|
|
if (!email) {
|
|
console.error('[LEANTIME_ADAPTER] Could not get user email from session');
|
|
return false;
|
|
}
|
|
|
|
const leantimeUserId = await this.getLeantimeUserId(email);
|
|
if (!leantimeUserId) {
|
|
console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email);
|
|
return false;
|
|
}
|
|
|
|
// Get all unread notifications
|
|
const notifications = await this.getNotifications(userId);
|
|
const unreadNotifications = notifications.filter(n => !n.isRead);
|
|
|
|
if (unreadNotifications.length === 0) {
|
|
// If there are no unread notifications, consider it a success
|
|
return true;
|
|
}
|
|
|
|
// Mark each notification as read individually
|
|
const promises = unreadNotifications.map(notification =>
|
|
this.markAsRead(userId, notification.id)
|
|
);
|
|
|
|
const results = await Promise.all(promises);
|
|
return results.every(result => result);
|
|
} catch (error) {
|
|
console.error('[LEANTIME_ADAPTER] Error marking all Leantime notifications as read:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async isConfigured(): Promise<boolean> {
|
|
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[] {
|
|
if (!Array.isArray(data)) {
|
|
return [];
|
|
}
|
|
|
|
return data.map(notification => ({
|
|
id: `${this.sourceName}-${notification.id}`,
|
|
source: this.sourceName as 'leantime',
|
|
sourceId: notification.id.toString(),
|
|
type: notification.type,
|
|
title: notification.type, // Leantime doesn't provide a separate title
|
|
message: notification.message,
|
|
link: notification.url,
|
|
isRead: notification.read === 1,
|
|
timestamp: new Date(notification.date),
|
|
priority: 'normal', // Default priority as Leantime doesn't specify
|
|
user: {
|
|
id: userId,
|
|
name: notification.username
|
|
},
|
|
metadata: {
|
|
moduleId: notification.moduleId
|
|
}
|
|
}));
|
|
}
|
|
|
|
// Helper function to get user's email directly from the session
|
|
private async getUserEmail(): Promise<string | null> {
|
|
console.log(`[LEANTIME_ADAPTER] Getting email from session`);
|
|
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session || !session.user?.email) {
|
|
console.error('[LEANTIME_ADAPTER] No session or email found');
|
|
return null;
|
|
}
|
|
|
|
console.log(`[LEANTIME_ADAPTER] Got email from session: ${session.user.email}`);
|
|
return session.user.email;
|
|
} catch (error) {
|
|
console.error('[LEANTIME_ADAPTER] Error getting user email from session:', 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<number | null> {
|
|
console.log(`[LEANTIME_ADAPTER] Getting Leantime userId for email: ${email}`);
|
|
|
|
try {
|
|
if (!this.apiToken) {
|
|
console.error('[LEANTIME_ADAPTER] LEANTIME_TOKEN is not set in environment variables');
|
|
return null;
|
|
}
|
|
|
|
console.log('[LEANTIME_ADAPTER] Sending request to get all Leantime users');
|
|
const jsonRpcBody = {
|
|
jsonrpc: '2.0',
|
|
method: 'leantime.rpc.users.getAll',
|
|
id: 1
|
|
};
|
|
console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody));
|
|
|
|
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': this.apiToken
|
|
},
|
|
body: JSON.stringify(jsonRpcBody),
|
|
});
|
|
|
|
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime users:', {
|
|
status: response.status,
|
|
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
|
|
});
|
|
return null;
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
console.log('[LEANTIME_ADAPTER] Raw response (truncated):', responseText.substring(0, 200) + (responseText.length > 200 ? '...' : ''));
|
|
|
|
const data = JSON.parse(responseText);
|
|
console.log('[LEANTIME_ADAPTER] Parsed response data:', {
|
|
hasResult: !!data.result,
|
|
resultIsArray: Array.isArray(data.result),
|
|
resultLength: Array.isArray(data.result) ? data.result.length : 'n/a',
|
|
error: data.error
|
|
});
|
|
|
|
if (!data.result || !Array.isArray(data.result)) {
|
|
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime users API');
|
|
return null;
|
|
}
|
|
|
|
const users = data.result;
|
|
console.log(`[LEANTIME_ADAPTER] Searching for user with email ${email} among ${users.length} users`);
|
|
|
|
// Log a few user objects to check structure
|
|
if (users.length > 0) {
|
|
console.log('[LEANTIME_ADAPTER] Sample user object:', JSON.stringify(users[0]));
|
|
}
|
|
|
|
const user = users.find((u: any) => u.username === email);
|
|
|
|
if (user) {
|
|
console.log('[LEANTIME_ADAPTER] Found Leantime user:', { id: user.id, username: user.username });
|
|
return user.id;
|
|
} else {
|
|
console.log('[LEANTIME_ADAPTER] No Leantime user found for email:', email);
|
|
// Try alternative property names that might contain the email
|
|
const alternativeUser = users.find((u: any) =>
|
|
u.email === email ||
|
|
u.mail === email ||
|
|
(typeof u.username === 'string' && u.username.toLowerCase() === email.toLowerCase())
|
|
);
|
|
|
|
if (alternativeUser) {
|
|
console.log('[LEANTIME_ADAPTER] Found Leantime user with alternative property match:', {
|
|
id: alternativeUser.id,
|
|
username: alternativeUser.username,
|
|
email: alternativeUser.email || alternativeUser.mail
|
|
});
|
|
return alternativeUser.id;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.error('[LEANTIME_ADAPTER] Error getting Leantime user ID:', error);
|
|
return null;
|
|
}
|
|
}
|
|
}
|