notifications

This commit is contained in:
alma 2025-05-04 11:22:35 +02:00
parent 8f523d65c0
commit 509f9ea0aa

View File

@ -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<Notification[]> {
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<NotificationCount> {
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<boolean> {
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<boolean> {
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<boolean> {
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<string | null> {
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<number | null> {
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<string | null> {
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;
}
}
}