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
JavaScript
/**
* 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 };