notifications
This commit is contained in:
parent
8f523d65c0
commit
509f9ea0aa
@ -1,6 +1,5 @@
|
|||||||
import { Notification, NotificationCount } from '@/lib/types/notification';
|
import { Notification, NotificationCount } from '@/lib/types/notification';
|
||||||
import { NotificationAdapter } from './notification-adapter.interface';
|
import { NotificationAdapter } from './notification-adapter.interface';
|
||||||
import { prisma } from '@/lib/prisma';
|
|
||||||
|
|
||||||
// Leantime notification type from their API
|
// Leantime notification type from their API
|
||||||
interface LeantimeNotification {
|
interface LeantimeNotification {
|
||||||
@ -18,53 +17,50 @@ interface LeantimeNotification {
|
|||||||
export class LeantimeAdapter implements NotificationAdapter {
|
export class LeantimeAdapter implements NotificationAdapter {
|
||||||
readonly sourceName = 'leantime';
|
readonly sourceName = 'leantime';
|
||||||
private apiUrl: string;
|
private apiUrl: string;
|
||||||
private apiKey: string;
|
private apiToken: string;
|
||||||
|
|
||||||
constructor() {
|
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.apiUrl = process.env.LEANTIME_API_URL || '';
|
||||||
this.apiKey = process.env.LEANTIME_API_KEY || '';
|
this.apiToken = process.env.LEANTIME_TOKEN || '';
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
|
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
|
||||||
const credentials = await this.getLeantimeCredentials(userId);
|
|
||||||
if (!credentials) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
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
|
// Calculate offset for pagination
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
// Make request to Leantime API
|
// Make request to Leantime API using jsonrpc
|
||||||
const response = await fetch(
|
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
||||||
`${credentials.url}/api/notifications?limit=${limit}&offset=${offset}`,
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${credentials.apiKey}`,
|
'X-API-Key': this.apiToken
|
||||||
'Content-Type': 'application/json'
|
},
|
||||||
}
|
body: JSON.stringify({
|
||||||
}
|
jsonrpc: '2.0',
|
||||||
);
|
method: 'leantime.rpc.notifications.getAll',
|
||||||
|
params: {
|
||||||
|
userId: leantimeUserId,
|
||||||
|
limit: limit,
|
||||||
|
offset: offset
|
||||||
|
},
|
||||||
|
id: 1
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(`Failed to fetch Leantime notifications: ${response.status}`);
|
console.error(`Failed to fetch Leantime notifications: ${response.status}`);
|
||||||
@ -72,7 +68,12 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching Leantime notifications:', error);
|
console.error('Error fetching Leantime notifications:', error);
|
||||||
return [];
|
return [];
|
||||||
@ -80,49 +81,50 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getNotificationCount(userId: string): Promise<NotificationCount> {
|
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 {
|
try {
|
||||||
// Make request to Leantime API
|
// First get the Leantime user ID from email
|
||||||
const response = await fetch(
|
const email = await this.getUserEmail(userId);
|
||||||
`${credentials.url}/api/notifications/count`,
|
if (!email) {
|
||||||
{
|
console.error('Could not get user email for userId:', userId);
|
||||||
headers: {
|
return this.getEmptyCount();
|
||||||
'Authorization': `Bearer ${credentials.apiKey}`,
|
}
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
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) {
|
if (!response.ok) {
|
||||||
console.error(`Failed to fetch Leantime notification count: ${response.status}`);
|
console.error(`Failed to fetch Leantime notification count: ${response.status}`);
|
||||||
return {
|
return this.getEmptyCount();
|
||||||
total: 0,
|
|
||||||
unread: 0,
|
|
||||||
sources: {
|
|
||||||
leantime: {
|
|
||||||
total: 0,
|
|
||||||
unread: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const unreadCount = data.unread || 0;
|
if (!data.result) {
|
||||||
const totalCount = data.total || 0;
|
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 {
|
return {
|
||||||
total: totalCount,
|
total: totalCount,
|
||||||
@ -136,42 +138,39 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching Leantime notification count:', error);
|
console.error('Error fetching Leantime notification count:', error);
|
||||||
return {
|
return this.getEmptyCount();
|
||||||
total: 0,
|
|
||||||
unread: 0,
|
|
||||||
sources: {
|
|
||||||
leantime: {
|
|
||||||
total: 0,
|
|
||||||
unread: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
|
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
|
||||||
const credentials = await this.getLeantimeCredentials(userId);
|
|
||||||
if (!credentials) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract the source ID from our compound ID
|
// Extract the source ID from our compound ID
|
||||||
const sourceId = notificationId.replace(`${this.sourceName}-`, '');
|
const sourceId = notificationId.replace(`${this.sourceName}-`, '');
|
||||||
|
|
||||||
// Make request to Leantime API
|
// Make request to Leantime API using jsonrpc
|
||||||
const response = await fetch(
|
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
||||||
`${credentials.url}/api/notifications/${sourceId}/read`,
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
method: 'POST',
|
'Content-Type': 'application/json',
|
||||||
headers: {
|
'X-API-Key': this.apiToken
|
||||||
'Authorization': `Bearer ${credentials.apiKey}`,
|
},
|
||||||
'Content-Type': 'application/json'
|
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) {
|
} catch (error) {
|
||||||
console.error('Error marking Leantime notification as read:', error);
|
console.error('Error marking Leantime notification as read:', error);
|
||||||
return false;
|
return false;
|
||||||
@ -179,25 +178,44 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async markAllAsRead(userId: string): Promise<boolean> {
|
async markAllAsRead(userId: string): Promise<boolean> {
|
||||||
const credentials = await this.getLeantimeCredentials(userId);
|
|
||||||
if (!credentials) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Make request to Leantime API
|
// First get the Leantime user ID from email
|
||||||
const response = await fetch(
|
const email = await this.getUserEmail(userId);
|
||||||
`${credentials.url}/api/notifications/read-all`,
|
if (!email) {
|
||||||
{
|
console.error('Could not get user email for userId:', userId);
|
||||||
method: 'POST',
|
return false;
|
||||||
headers: {
|
}
|
||||||
'Authorization': `Bearer ${credentials.apiKey}`,
|
|
||||||
'Content-Type': 'application/json'
|
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) {
|
} catch (error) {
|
||||||
console.error('Error marking all Leantime notifications as read:', error);
|
console.error('Error marking all Leantime notifications as read:', error);
|
||||||
return false;
|
return false;
|
||||||
@ -205,7 +223,20 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isConfigured(): Promise<boolean> {
|
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[] {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user