UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

534 lines (466 loc) 15 kB
/** * Notifications Service * Implements API and operators for notifications with foreign key relationships * Uses notifications entity with workspace/account and subscription/cost pool references */ import tokenAuthService from './tokenAuth.js'; import { getRootApiEndpoint } from './centralizedApi.js'; class NotificationsService { constructor() { this.authService = tokenAuthService; this.apiEndpoint = `${getRootApiEndpoint()}/notifications`; this.subscriptionsCache = new Map(); this.operatorsCache = new Map(); } /** * Get notifications for the authenticated user * Uses smart token system to coordinate authentication */ async getNotifications(options = {}) { const defaultOptions = { page: 1, limit: 50, status: 'all', // 'unread', 'read', 'all' type: 'all', // 'system', 'billing', 'technical', 'all' sortBy: 'created_at', sortOrder: 'desc', }; const params = { ...defaultOptions, ...options }; try { // Smart token system - get token (will reuse existing or coordinate generation) const token = await this.authService.getToken(); if (!token) { throw new Error('Authentication failed'); } const queryString = new URLSearchParams(params).toString(); const response = await fetch(`${this.apiEndpoint}?${queryString}`, { method: 'GET', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); // Filter notifications based on foreign key access const accessibleNotifications = await this.filterByAccess( result.notifications || [] ); return { notifications: accessibleNotifications, total: result.total, page: result.page, totalPages: result.totalPages, }; } catch (error) { console.error('Error fetching notifications:', error); throw error; } } /** * Filter notifications based on user's access to related entities */ async filterByAccess(notifications) { const accessibleNotifications = []; for (const notification of notifications) { try { // Check access to related workspace/account const hasWorkspaceAccess = notification.workspace_id ? await this.authService.canAccessEntity( 'workspace', notification.workspace_id, ['read'] ) : true; const hasAccountAccess = notification.account_id ? await this.authService.canAccessEntity( 'account', notification.account_id, ['read'] ) : true; // Check access to related subscription/cost pool const hasSubscriptionAccess = notification.subscription_id ? await this.authService.canAccessEntity( 'subscription', notification.subscription_id, ['read'] ) : true; if (hasWorkspaceAccess && hasAccountAccess && hasSubscriptionAccess) { accessibleNotifications.push(notification); } } catch (error) { console.warn( `Failed to check access for notification ${notification.id}:`, error ); } } return accessibleNotifications; } /** * Create a new notification with foreign key relationships */ async createNotification(notificationData) { try { if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required'); } // Ensure required foreign key relationships are provided const notification = { ...notificationData, created_at: new Date().toISOString(), status: 'unread', created_by: this.authService.getUserPermission('userId'), // Foreign key relationships (at least one must be provided) workspace_id: notificationData.workspace_id || this.authService.getUserPermission('workspaceId'), account_id: notificationData.account_id || this.authService.getUserPermission('accountId'), subscription_id: notificationData.subscription_id || this.authService.getUserPermission('subscriptionId'), }; // Validate that at least one foreign key relationship exists if ( !notification.workspace_id && !notification.account_id && !notification.subscription_id ) { throw new Error( 'At least one foreign key relationship (workspace, account, or subscription) is required' ); } const response = await fetch(this.apiEndpoint, { method: 'POST', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(notification), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); // Subscribe to real-time updates for this notification await this.subscribeToNotification(result.id); return result; } catch (error) { console.error('Error creating notification:', error); throw error; } } /** * Update notification status (read/unread) */ async updateNotificationStatus(notificationId, status) { try { if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required'); } // Verify access to this notification first const canAccess = await this.authService.canAccessEntity( 'notification', notificationId, ['write'] ); if (!canAccess) { throw new Error('Access denied to this notification'); } const response = await fetch( `${this.apiEndpoint}/${notificationId}/status`, { method: 'PATCH', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ status }), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error updating notification status:', error); throw error; } } /** * Delete notification (if user has permission) */ async deleteNotification(notificationId) { try { if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required'); } // Verify access to this notification first const canAccess = await this.authService.canAccessEntity( 'notification', notificationId, ['delete'] ); if (!canAccess) { throw new Error('Access denied to delete this notification'); } const response = await fetch(`${this.apiEndpoint}/${notificationId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${this.authService.getToken()}`, }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return { success: true }; } catch (error) { console.error('Error deleting notification:', error); throw error; } } /** * Mark all notifications as read for user's accessible entities */ async markAllAsRead() { try { if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required'); } const response = await fetch(`${this.apiEndpoint}/mark-all-read`, { method: 'POST', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error marking all notifications as read:', error); throw error; } } /** * Subscribe to real-time notification updates */ async subscribeToNotification(notificationId) { try { // Use WebSocket or SignalR for real-time updates if (this.subscriptionsCache.has(notificationId)) { return; // Already subscribed } const subscription = { notificationId, timestamp: Date.now(), }; this.subscriptionsCache.set(notificationId, subscription); // Set up real-time connection if not already established await this.ensureRealtimeConnection(); } catch (error) { console.error('Error subscribing to notification updates:', error); } } /** * Ensure real-time connection for notification updates */ async ensureRealtimeConnection() { if ( this.realtimeConnection && this.realtimeConnection.state === 'Connected' ) { return; } try { // Initialize SignalR connection for real-time notifications if (typeof window !== 'undefined' && window.signalR) { this.realtimeConnection = new window.signalR.HubConnectionBuilder() .withUrl(`${getRootApiEndpoint()}/notifications-hub`, { accessTokenFactory: () => this.authService.getToken(), }) .build(); // Set up event handlers this.realtimeConnection.on( 'NotificationCreated', this.handleNotificationCreated.bind(this) ); this.realtimeConnection.on( 'NotificationUpdated', this.handleNotificationUpdated.bind(this) ); this.realtimeConnection.on( 'NotificationDeleted', this.handleNotificationDeleted.bind(this) ); await this.realtimeConnection.start(); console.log('[Notifications] Real-time connection established'); } } catch (error) { console.error('Error establishing real-time connection:', error); } } /** * Handle new notification received via real-time connection */ handleNotificationCreated(notification) { // Check if user has access to this notification based on foreign keys this.authService .canAccessEntity('notification', notification.id, ['read']) .then(canAccess => { if (canAccess) { // Emit custom event for UI components to handle window.dispatchEvent( new CustomEvent('notificationCreated', { detail: notification, }) ); } }) .catch(error => { console.warn('Error checking notification access:', error); }); } /** * Handle notification update received via real-time connection */ handleNotificationUpdated(notification) { this.authService .canAccessEntity('notification', notification.id, ['read']) .then(canAccess => { if (canAccess) { window.dispatchEvent( new CustomEvent('notificationUpdated', { detail: notification, }) ); } }) .catch(error => { console.warn('Error checking notification access:', error); }); } /** * Handle notification deletion received via real-time connection */ handleNotificationDeleted(notificationId) { window.dispatchEvent( new CustomEvent('notificationDeleted', { detail: { id: notificationId }, }) ); } /** * Get notification operators/actions available for user */ async getAvailableOperators() { try { if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required'); } // Check cache first const cacheKey = this.authService.getUserPermission('userId'); if (this.operatorsCache.has(cacheKey)) { return this.operatorsCache.get(cacheKey); } const response = await fetch(`${this.apiEndpoint}/operators`, { method: 'GET', headers: { Authorization: `Bearer ${this.authService.getToken()}`, }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const operators = await response.json(); this.operatorsCache.set(cacheKey, operators); return operators; } catch (error) { console.error('Error fetching notification operators:', error); throw error; } } /** * Execute notification operator/action */ async executeOperator(notificationId, operatorType, operatorData = {}) { try { if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required'); } // Verify access to notification first const canAccess = await this.authService.canAccessEntity( 'notification', notificationId, ['write'] ); if (!canAccess) { throw new Error( 'Access denied to execute operation on this notification' ); } const response = await fetch( `${this.apiEndpoint}/${notificationId}/operators/${operatorType}`, { method: 'POST', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(operatorData), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error executing notification operator:', error); throw error; } } /** * Get notification statistics for user's accessible entities */ async getNotificationStats() { try { if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required'); } const response = await fetch(`${this.apiEndpoint}/stats`, { method: 'GET', headers: { Authorization: `Bearer ${this.authService.getToken()}`, }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error fetching notification stats:', error); throw error; } } /** * Cleanup resources */ destroy() { if (this.realtimeConnection) { this.realtimeConnection.stop(); } this.subscriptionsCache.clear(); this.operatorsCache.clear(); } } // Create singleton instance const notificationsService = new NotificationsService(); export default notificationsService; export { NotificationsService };