notifications
This commit is contained in:
parent
509f9ea0aa
commit
fc1dd882b6
83
app/api/debug/notifications/route.ts
Normal file
83
app/api/debug/notifications/route.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
|
import { NotificationService } from '@/lib/services/notifications/notification-service';
|
||||||
|
|
||||||
|
// GET /api/debug/notifications
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
// Authenticate user
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session || !session.user?.id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Not authenticated" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id;
|
||||||
|
console.log(`[DEBUG] Testing notifications for user ${userId}`);
|
||||||
|
|
||||||
|
// Get environment variables status
|
||||||
|
const envStatus = {
|
||||||
|
LEANTIME_API_URL: process.env.LEANTIME_API_URL ? 'Set' : 'Not set',
|
||||||
|
LEANTIME_TOKEN: process.env.LEANTIME_TOKEN ? `Set (length: ${process.env.LEANTIME_TOKEN.length})` : 'Not set',
|
||||||
|
LEANTIME_API_KEY: process.env.LEANTIME_API_KEY ? `Set (length: ${process.env.LEANTIME_API_KEY.length})` : 'Not set',
|
||||||
|
KEYCLOAK_BASE_URL: process.env.KEYCLOAK_BASE_URL ? 'Set' : 'Not set',
|
||||||
|
KEYCLOAK_REALM: process.env.KEYCLOAK_REALM || 'Not set',
|
||||||
|
KEYCLOAK_ADMIN_CLIENT_ID: process.env.KEYCLOAK_ADMIN_CLIENT_ID ? 'Set' : 'Not set',
|
||||||
|
KEYCLOAK_ADMIN_CLIENT_SECRET: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET ? 'Set (masked)' : 'Not set',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get user information
|
||||||
|
console.log(`[DEBUG] Getting user info for ${userId}`);
|
||||||
|
let userInfo = {
|
||||||
|
id: userId,
|
||||||
|
email: session.user.email || 'Unknown'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test notification service
|
||||||
|
console.log(`[DEBUG] Testing notification service`);
|
||||||
|
const notificationService = NotificationService.getInstance();
|
||||||
|
|
||||||
|
// Get notification count
|
||||||
|
console.log(`[DEBUG] Getting notification count`);
|
||||||
|
const startTimeCount = Date.now();
|
||||||
|
const notificationCount = await notificationService.getNotificationCount(userId);
|
||||||
|
const timeForCount = Date.now() - startTimeCount;
|
||||||
|
|
||||||
|
// Get notifications
|
||||||
|
console.log(`[DEBUG] Getting notifications`);
|
||||||
|
const startTimeNotifications = Date.now();
|
||||||
|
const notifications = await notificationService.getNotifications(userId, 1, 10);
|
||||||
|
const timeForNotifications = Date.now() - startTimeNotifications;
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
userInfo,
|
||||||
|
environmentVariables: envStatus,
|
||||||
|
notificationServiceTest: {
|
||||||
|
count: {
|
||||||
|
result: notificationCount,
|
||||||
|
timeMs: timeForCount
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
count: notifications.length,
|
||||||
|
timeMs: timeForNotifications,
|
||||||
|
samples: notifications.slice(0, 3) // Only return first 3 notifications as samples
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[DEBUG] Error in debug notifications API:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Internal server error",
|
||||||
|
message: error.message,
|
||||||
|
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,20 +23,33 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
// Load from environment variables, matching the pattern used in other Leantime integrations
|
// 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.apiToken = process.env.LEANTIME_TOKEN || '';
|
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[]> {
|
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
|
||||||
|
console.log(`[LEANTIME_ADAPTER] getNotifications called for userId: ${userId}, page: ${page}, limit: ${limit}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First get the Leantime user ID from email
|
// First get the Leantime user ID from email
|
||||||
const email = await this.getUserEmail(userId);
|
const email = await this.getUserEmail(userId);
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Retrieved email for userId ${userId}:`, email || 'null');
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
console.error('Could not get user email for userId:', userId);
|
console.error('[LEANTIME_ADAPTER] Could not get user email for userId:', userId);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const leantimeUserId = await this.getLeantimeUserId(email);
|
const leantimeUserId = await this.getLeantimeUserId(email);
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Retrieved Leantime userId for email ${email}:`, leantimeUserId || 'null');
|
||||||
|
|
||||||
if (!leantimeUserId) {
|
if (!leantimeUserId) {
|
||||||
console.error('User not found in Leantime:', email);
|
console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,88 +57,137 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
// Make request to Leantime API using jsonrpc
|
// 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.getAll',
|
||||||
|
params: {
|
||||||
|
userId: leantimeUserId,
|
||||||
|
limit: limit,
|
||||||
|
offset: offset
|
||||||
|
},
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody));
|
||||||
|
|
||||||
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-API-Key': this.apiToken
|
'X-API-Key': this.apiToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(jsonRpcBody)
|
||||||
jsonrpc: '2.0',
|
|
||||||
method: 'leantime.rpc.notifications.getAll',
|
|
||||||
params: {
|
|
||||||
userId: leantimeUserId,
|
|
||||||
limit: limit,
|
|
||||||
offset: offset
|
|
||||||
},
|
|
||||||
id: 1
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(`Failed to fetch Leantime notifications: ${response.status}`);
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
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.result || !Array.isArray(data.result)) {
|
||||||
console.error('Invalid response format from Leantime notifications API');
|
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime notifications API');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.transformNotifications(data.result, userId);
|
const notifications = this.transformNotifications(data.result, userId);
|
||||||
|
console.log('[LEANTIME_ADAPTER] Transformed notifications count:', notifications.length);
|
||||||
|
return notifications;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching Leantime notifications:', error);
|
console.error('[LEANTIME_ADAPTER] Error fetching Leantime notifications:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNotificationCount(userId: string): Promise<NotificationCount> {
|
async getNotificationCount(userId: string): Promise<NotificationCount> {
|
||||||
|
console.log(`[LEANTIME_ADAPTER] getNotificationCount called for userId: ${userId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First get the Leantime user ID from email
|
// First get the Leantime user ID from email
|
||||||
const email = await this.getUserEmail(userId);
|
const email = await this.getUserEmail(userId);
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Retrieved email for userId ${userId}:`, email || 'null');
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
console.error('Could not get user email for userId:', userId);
|
console.error('[LEANTIME_ADAPTER] Could not get user email for userId:', userId);
|
||||||
return this.getEmptyCount();
|
return this.getEmptyCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
const leantimeUserId = await this.getLeantimeUserId(email);
|
const leantimeUserId = await this.getLeantimeUserId(email);
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Retrieved Leantime userId for email ${email}:`, leantimeUserId || 'null');
|
||||||
|
|
||||||
if (!leantimeUserId) {
|
if (!leantimeUserId) {
|
||||||
console.error('User not found in Leantime:', email);
|
console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email);
|
||||||
return this.getEmptyCount();
|
return this.getEmptyCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make request to Leantime API using jsonrpc
|
// Make request to Leantime API using jsonrpc
|
||||||
|
console.log('[LEANTIME_ADAPTER] Sending request to Leantime API for notification count');
|
||||||
|
const jsonRpcBody = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'leantime.rpc.notifications.getNotificationCount',
|
||||||
|
params: {
|
||||||
|
userId: leantimeUserId
|
||||||
|
},
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody));
|
||||||
|
|
||||||
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-API-Key': this.apiToken
|
'X-API-Key': this.apiToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(jsonRpcBody)
|
||||||
jsonrpc: '2.0',
|
|
||||||
method: 'leantime.rpc.notifications.getNotificationCount',
|
|
||||||
params: {
|
|
||||||
userId: leantimeUserId
|
|
||||||
},
|
|
||||||
id: 1
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(`Failed to fetch Leantime notification count: ${response.status}`);
|
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();
|
return this.getEmptyCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const responseText = await response.text();
|
||||||
|
console.log('[LEANTIME_ADAPTER] Raw response:', responseText);
|
||||||
|
|
||||||
|
const data = JSON.parse(responseText);
|
||||||
|
console.log('[LEANTIME_ADAPTER] Parsed response data:', {
|
||||||
|
hasResult: !!data.result,
|
||||||
|
resultData: data.result,
|
||||||
|
error: data.error
|
||||||
|
});
|
||||||
|
|
||||||
if (!data.result) {
|
if (!data.result) {
|
||||||
console.error('Invalid response format from Leantime notification count API');
|
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime notification count API');
|
||||||
return this.getEmptyCount();
|
return this.getEmptyCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
const unreadCount = data.result.unread || 0;
|
const unreadCount = data.result.unread || 0;
|
||||||
const totalCount = data.result.total || 0;
|
const totalCount = data.result.total || 0;
|
||||||
|
|
||||||
|
console.log('[LEANTIME_ADAPTER] Notification counts:', { unread: unreadCount, total: totalCount });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: totalCount,
|
total: totalCount,
|
||||||
unread: unreadCount,
|
unread: unreadCount,
|
||||||
@ -137,7 +199,7 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching Leantime notification count:', error);
|
console.error('[LEANTIME_ADAPTER] Error fetching Leantime notification count:', error);
|
||||||
return this.getEmptyCount();
|
return this.getEmptyCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -267,36 +329,69 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
|
|
||||||
// Helper function to get user's email from ID (using existing method from your system)
|
// Helper function to get user's email from ID (using existing method from your system)
|
||||||
private async getUserEmail(userId: string): Promise<string | null> {
|
private async getUserEmail(userId: string): Promise<string | null> {
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Getting email for userId: ${userId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch from Keycloak similar to how other components do
|
// 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}`, {
|
const adminToken = await this.getAdminToken();
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Got admin token for Keycloak:`, adminToken ? 'token obtained' : 'failed to get token');
|
||||||
|
|
||||||
|
if (!adminToken) {
|
||||||
|
console.error('[LEANTIME_ADAPTER] Failed to get admin token for Keycloak');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keycloakUrl = `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}`;
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Fetching user from Keycloak:`, keycloakUrl);
|
||||||
|
|
||||||
|
const response = await fetch(keycloakUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${await this.getAdminToken()}`
|
'Authorization': `Bearer ${adminToken}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Keycloak response status:`, response.status);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Failed to get user from Keycloak');
|
const errorText = await response.text();
|
||||||
|
console.error('[LEANTIME_ADAPTER] Failed to get user from Keycloak:', {
|
||||||
|
status: response.status,
|
||||||
|
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData = await response.json();
|
const userData = await response.json();
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Got user data from Keycloak:`, {
|
||||||
|
id: userData.id,
|
||||||
|
email: userData.email,
|
||||||
|
username: userData.username
|
||||||
|
});
|
||||||
|
|
||||||
return userData.email || null;
|
return userData.email || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting user email:', error);
|
console.error('[LEANTIME_ADAPTER] Error getting user email:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get Leantime user ID by email (using similar pattern to the tasks API)
|
// Helper function to get Leantime user ID by email (using similar pattern to the tasks API)
|
||||||
private async getLeantimeUserId(email: string): Promise<number | null> {
|
private async getLeantimeUserId(email: string): Promise<number | null> {
|
||||||
|
console.log(`[LEANTIME_ADAPTER] Getting Leantime userId for email: ${email}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.apiToken) {
|
if (!this.apiToken) {
|
||||||
console.error('LEANTIME_TOKEN is not set in environment variables');
|
console.error('[LEANTIME_ADAPTER] LEANTIME_TOKEN is not set in environment variables');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Fetching Leantime user for email:', email);
|
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`, {
|
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -304,46 +399,85 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-API-Key': this.apiToken
|
'X-API-Key': this.apiToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(jsonRpcBody),
|
||||||
jsonrpc: '2.0',
|
|
||||||
method: 'leantime.rpc.users.getAll',
|
|
||||||
id: 1
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Failed to fetch Leantime users');
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
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)) {
|
if (!data.result || !Array.isArray(data.result)) {
|
||||||
console.error('Invalid response format from Leantime users API');
|
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime users API');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = data.result;
|
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);
|
const user = users.find((u: any) => u.username === email);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
console.log('Found Leantime user:', { id: user.id, username: user.username });
|
console.log('[LEANTIME_ADAPTER] Found Leantime user:', { id: user.id, username: user.username });
|
||||||
return user.id;
|
return user.id;
|
||||||
} else {
|
} else {
|
||||||
console.log('No Leantime user found for email:', email);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting Leantime user ID:', error);
|
console.error('[LEANTIME_ADAPTER] Error getting Leantime user ID:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get admin token (similar to the user management API)
|
// Helper function to get admin token (similar to the user management API)
|
||||||
private async getAdminToken(): Promise<string | null> {
|
private async getAdminToken(): Promise<string | null> {
|
||||||
|
console.log('[LEANTIME_ADAPTER] Getting admin token from Keycloak');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const keycloakTokenUrl = `${process.env.KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token`;
|
||||||
|
console.log('[LEANTIME_ADAPTER] Keycloak token URL:', keycloakTokenUrl);
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${process.env.KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token`,
|
keycloakTokenUrl,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -357,15 +491,23 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('[LEANTIME_ADAPTER] Keycloak token response status:', response.status);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Failed to get admin token');
|
const errorText = await response.text();
|
||||||
|
console.error('[LEANTIME_ADAPTER] Failed to get admin token:', {
|
||||||
|
status: response.status,
|
||||||
|
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenData = await response.json();
|
const tokenData = await response.json();
|
||||||
|
console.log('[LEANTIME_ADAPTER] Successfully obtained admin token');
|
||||||
|
|
||||||
return tokenData.access_token;
|
return tokenData.access_token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting admin token:', error);
|
console.error('[LEANTIME_ADAPTER] Error getting admin token:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,8 @@ export class NotificationService {
|
|||||||
private static REFRESH_LOCK_TTL = 30; // 30 seconds
|
private static REFRESH_LOCK_TTL = 30; // 30 seconds
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
console.log('[NOTIFICATION_SERVICE] Initializing notification service');
|
||||||
|
|
||||||
// Register adapters
|
// Register adapters
|
||||||
this.registerAdapter(new LeantimeAdapter());
|
this.registerAdapter(new LeantimeAdapter());
|
||||||
|
|
||||||
@ -25,6 +27,8 @@ export class NotificationService {
|
|||||||
// this.registerAdapter(new GiteaAdapter());
|
// this.registerAdapter(new GiteaAdapter());
|
||||||
// this.registerAdapter(new DolibarrAdapter());
|
// this.registerAdapter(new DolibarrAdapter());
|
||||||
// this.registerAdapter(new MoodleAdapter());
|
// this.registerAdapter(new MoodleAdapter());
|
||||||
|
|
||||||
|
console.log('[NOTIFICATION_SERVICE] Registered adapters:', Array.from(this.adapters.keys()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,6 +36,7 @@ export class NotificationService {
|
|||||||
*/
|
*/
|
||||||
public static getInstance(): NotificationService {
|
public static getInstance(): NotificationService {
|
||||||
if (!NotificationService.instance) {
|
if (!NotificationService.instance) {
|
||||||
|
console.log('[NOTIFICATION_SERVICE] Creating new notification service instance');
|
||||||
NotificationService.instance = new NotificationService();
|
NotificationService.instance = new NotificationService();
|
||||||
}
|
}
|
||||||
return NotificationService.instance;
|
return NotificationService.instance;
|
||||||
@ -42,13 +47,14 @@ export class NotificationService {
|
|||||||
*/
|
*/
|
||||||
private registerAdapter(adapter: NotificationAdapter): void {
|
private registerAdapter(adapter: NotificationAdapter): void {
|
||||||
this.adapters.set(adapter.sourceName, adapter);
|
this.adapters.set(adapter.sourceName, adapter);
|
||||||
console.log(`Registered notification adapter: ${adapter.sourceName}`);
|
console.log(`[NOTIFICATION_SERVICE] Registered notification adapter: ${adapter.sourceName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all notifications for a user from all configured sources
|
* Get all notifications for a user from all configured sources
|
||||||
*/
|
*/
|
||||||
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
|
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] getNotifications called for user ${userId}, page ${page}, limit ${limit}`);
|
||||||
const redis = getRedisClient();
|
const redis = getRedisClient();
|
||||||
const cacheKey = NotificationService.NOTIFICATIONS_LIST_CACHE_KEY(userId, page, limit);
|
const cacheKey = NotificationService.NOTIFICATIONS_LIST_CACHE_KEY(userId, page, limit);
|
||||||
|
|
||||||
@ -67,31 +73,49 @@ export class NotificationService {
|
|||||||
return JSON.parse(cachedData);
|
return JSON.parse(cachedData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error retrieving notifications from cache:', error);
|
console.error('[NOTIFICATION_SERVICE] Error retrieving notifications from cache:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No cached data, fetch from all adapters
|
// No cached data, fetch from all adapters
|
||||||
console.log(`[NOTIFICATION_SERVICE] Fetching notifications for user ${userId}`);
|
console.log(`[NOTIFICATION_SERVICE] Fetching notifications for user ${userId} from ${this.adapters.size} adapters`);
|
||||||
|
|
||||||
const allNotifications: Notification[] = [];
|
const allNotifications: Notification[] = [];
|
||||||
const promises = Array.from(this.adapters.values())
|
const adapterEntries = Array.from(this.adapters.entries());
|
||||||
.map(adapter => adapter.isConfigured()
|
console.log(`[NOTIFICATION_SERVICE] Available adapters: ${adapterEntries.map(([name]) => name).join(', ')}`);
|
||||||
.then(configured => configured ? adapter.getNotifications(userId, page, limit) : [])
|
|
||||||
.catch(error => {
|
const promises = adapterEntries.map(async ([name, adapter]) => {
|
||||||
console.error(`Error fetching notifications from ${adapter.sourceName}:`, error);
|
console.log(`[NOTIFICATION_SERVICE] Checking if adapter ${name} is configured`);
|
||||||
|
try {
|
||||||
|
const configured = await adapter.isConfigured();
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Adapter ${name} is configured: ${configured}`);
|
||||||
|
|
||||||
|
if (configured) {
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Fetching notifications from ${name} for user ${userId}`);
|
||||||
|
const notifications = await adapter.getNotifications(userId, page, limit);
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Got ${notifications.length} notifications from ${name}`);
|
||||||
|
return notifications;
|
||||||
|
} else {
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Skipping adapter ${name} as it is not configured`);
|
||||||
return [];
|
return [];
|
||||||
})
|
}
|
||||||
);
|
} catch (error) {
|
||||||
|
console.error(`[NOTIFICATION_SERVICE] Error fetching notifications from ${name}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
// Combine all notifications
|
// Combine all notifications
|
||||||
results.forEach(notifications => {
|
results.forEach((notifications, index) => {
|
||||||
|
const adapterName = adapterEntries[index][0];
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Adding ${notifications.length} notifications from ${adapterName}`);
|
||||||
allNotifications.push(...notifications);
|
allNotifications.push(...notifications);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort by timestamp (newest first)
|
// Sort by timestamp (newest first)
|
||||||
allNotifications.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
allNotifications.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Total notifications after sorting: ${allNotifications.length}`);
|
||||||
|
|
||||||
// Store in cache
|
// Store in cache
|
||||||
try {
|
try {
|
||||||
@ -101,8 +125,9 @@ export class NotificationService {
|
|||||||
'EX',
|
'EX',
|
||||||
NotificationService.LIST_CACHE_TTL
|
NotificationService.LIST_CACHE_TTL
|
||||||
);
|
);
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Cached ${allNotifications.length} notifications for user ${userId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error caching notifications:', error);
|
console.error('[NOTIFICATION_SERVICE] Error caching notifications:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allNotifications;
|
return allNotifications;
|
||||||
@ -112,6 +137,7 @@ export class NotificationService {
|
|||||||
* Get notification counts for a user
|
* Get notification counts for a user
|
||||||
*/
|
*/
|
||||||
async getNotificationCount(userId: string): Promise<NotificationCount> {
|
async getNotificationCount(userId: string): Promise<NotificationCount> {
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] getNotificationCount called for user ${userId}`);
|
||||||
const redis = getRedisClient();
|
const redis = getRedisClient();
|
||||||
const cacheKey = NotificationService.NOTIFICATION_COUNT_CACHE_KEY(userId);
|
const cacheKey = NotificationService.NOTIFICATION_COUNT_CACHE_KEY(userId);
|
||||||
|
|
||||||
@ -130,11 +156,11 @@ export class NotificationService {
|
|||||||
return JSON.parse(cachedData);
|
return JSON.parse(cachedData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error retrieving notification counts from cache:', error);
|
console.error('[NOTIFICATION_SERVICE] Error retrieving notification counts from cache:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No cached data, fetch counts from all adapters
|
// No cached data, fetch counts from all adapters
|
||||||
console.log(`[NOTIFICATION_SERVICE] Fetching notification counts for user ${userId}`);
|
console.log(`[NOTIFICATION_SERVICE] Fetching notification counts for user ${userId} from ${this.adapters.size} adapters`);
|
||||||
|
|
||||||
const aggregatedCount: NotificationCount = {
|
const aggregatedCount: NotificationCount = {
|
||||||
total: 0,
|
total: 0,
|
||||||
@ -142,21 +168,39 @@ export class NotificationService {
|
|||||||
sources: {}
|
sources: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises = Array.from(this.adapters.values())
|
const adapterEntries = Array.from(this.adapters.entries());
|
||||||
.map(adapter => adapter.isConfigured()
|
console.log(`[NOTIFICATION_SERVICE] Available adapters for count: ${adapterEntries.map(([name]) => name).join(', ')}`);
|
||||||
.then(configured => configured ? adapter.getNotificationCount(userId) : null)
|
|
||||||
.catch(error => {
|
const promises = adapterEntries.map(async ([name, adapter]) => {
|
||||||
console.error(`Error fetching notification count from ${adapter.sourceName}:`, error);
|
console.log(`[NOTIFICATION_SERVICE] Checking if adapter ${name} is configured for count`);
|
||||||
|
try {
|
||||||
|
const configured = await adapter.isConfigured();
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Adapter ${name} is configured for count: ${configured}`);
|
||||||
|
|
||||||
|
if (configured) {
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Fetching notification count from ${name} for user ${userId}`);
|
||||||
|
const count = await adapter.getNotificationCount(userId);
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Got count from ${name}:`, count);
|
||||||
|
return count;
|
||||||
|
} else {
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Skipping adapter ${name} for count as it is not configured`);
|
||||||
return null;
|
return null;
|
||||||
})
|
}
|
||||||
);
|
} catch (error) {
|
||||||
|
console.error(`[NOTIFICATION_SERVICE] Error fetching notification count from ${name}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
// Combine all counts
|
// Combine all counts
|
||||||
results.forEach(count => {
|
results.forEach((count, index) => {
|
||||||
if (!count) return;
|
if (!count) return;
|
||||||
|
|
||||||
|
const adapterName = adapterEntries[index][0];
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Adding counts from ${adapterName}: total=${count.total}, unread=${count.unread}`);
|
||||||
|
|
||||||
aggregatedCount.total += count.total;
|
aggregatedCount.total += count.total;
|
||||||
aggregatedCount.unread += count.unread;
|
aggregatedCount.unread += count.unread;
|
||||||
|
|
||||||
@ -166,6 +210,8 @@ export class NotificationService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Aggregated counts for user ${userId}:`, aggregatedCount);
|
||||||
|
|
||||||
// Store in cache
|
// Store in cache
|
||||||
try {
|
try {
|
||||||
await redis.set(
|
await redis.set(
|
||||||
@ -174,8 +220,9 @@ export class NotificationService {
|
|||||||
'EX',
|
'EX',
|
||||||
NotificationService.COUNT_CACHE_TTL
|
NotificationService.COUNT_CACHE_TTL
|
||||||
);
|
);
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Cached notification counts for user ${userId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error caching notification counts:', error);
|
console.error('[NOTIFICATION_SERVICE] Error caching notification counts:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return aggregatedCount;
|
return aggregatedCount;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user