UNPKG

besper-frontend-site-dev-main

Version:

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

1,027 lines (893 loc) 29 kB
/** * Page Operators Service * Provides secure, decorated functions for all data actions across pages * Uses authentication and authorization checks for all operations */ import tokenAuthService from './tokenAuth.js'; import supportTicketsService from './supportTicketsService.js'; import notificationsService from './notificationsService.js'; import { getRootApiEndpoint } from './centralizedApi.js'; class PageOperatorsService { constructor() { this.authService = tokenAuthService; this.supportTicketsService = supportTicketsService; this.notificationsService = notificationsService; this.operatorCache = new Map(); } /** * Decorator for authentication and authorization */ withAuth(operation) { return async (...args) => { try { // Check authentication if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required for this operation'); } // Verify contact access (for entities without Dataverse equivalents) const contactId = this.authService.getUserPermission('contactId'); if (contactId) { const canAccessContact = await this.authService.canAccessEntity( 'contact', contactId, ['read'] ); if (!canAccessContact) { throw new Error( 'Contact verification failed - operation not permitted' ); } } // Execute the operation return await operation(...args); } catch (error) { console.error('Operator failed:', error); throw error; } }; } /** * Decorator for workspace-level authorization */ withWorkspaceAuth(requiredPermissions = ['read']) { return operation => { return async (entityData, ...args) => { try { // Check authentication first if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required for this operation'); } // Check workspace access via foreign key relationships const workspaceId = entityData.workspace_id || this.authService.getUserPermission('workspaceId'); const accountId = entityData.account_id || this.authService.getUserPermission('accountId'); const subscriptionId = entityData.subscription_id || this.authService.getUserPermission('subscriptionId'); let hasAccess = false; // Check access to workspace if (workspaceId) { hasAccess = await this.authService.canAccessEntity( 'workspace', workspaceId, requiredPermissions ); } // Check access to account if workspace access failed if (!hasAccess && accountId) { hasAccess = await this.authService.canAccessEntity( 'account', accountId, requiredPermissions ); } // Check access to subscription if other access failed if (!hasAccess && subscriptionId) { hasAccess = await this.authService.canAccessEntity( 'subscription', subscriptionId, requiredPermissions ); } if (!hasAccess) { throw new Error( 'Access denied - insufficient permissions for related workspace/account/subscription' ); } // Execute the operation return await operation(entityData, ...args); } catch (error) { console.error('Workspace-authorized operator failed:', error); throw error; } }; }; } // ======================================== // MY BOTS PAGE OPERATORS // ======================================== /** * Get user's accessible bots */ getBots = this.withAuth(async () => { const response = await fetch(`${getRootApiEndpoint()}/bots`, { method: 'GET', 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(); }); /** * Create new bot */ createBot = this.withWorkspaceAuth(['create'])(async botData => { const response = await fetch(`${getRootApiEndpoint()}/bots`, { method: 'POST', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(botData), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); }); /** * Update bot configuration */ updateBot = this.withWorkspaceAuth(['write'])(async botData => { const response = await fetch(`${getRootApiEndpoint()}/bots/${botData.id}`, { method: 'PUT', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(botData), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); }); /** * Delete bot */ deleteBot = this.withWorkspaceAuth(['delete'])(async botData => { const response = await fetch(`${getRootApiEndpoint()}/bots/${botData.id}`, { method: 'DELETE', 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(); }); /** * Get bot configuration */ getBotConfig = this.withWorkspaceAuth(['read'])(async botData => { const response = await fetch( `${getRootApiEndpoint()}/bots/${botData.id}/config`, { method: 'GET', 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(); }); /** * Update bot configuration */ updateBotConfig = this.withWorkspaceAuth(['write'])( async (botData, configData) => { const response = await fetch( `${getRootApiEndpoint()}/bots/${botData.id}/config`, { method: 'PUT', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(configData), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } ); // ======================================== // USERS PAGE OPERATORS // ======================================== /** * Get workspace users */ getWorkspaceUsers = this.withWorkspaceAuth(['read'])(async workspaceData => { const response = await fetch( `${getRootApiEndpoint()}/workspaces/${workspaceData.workspace_id}/users`, { method: 'GET', 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(); }); /** * Invite user to workspace */ inviteUser = this.withWorkspaceAuth(['write'])( async (workspaceData, inviteData) => { const response = await fetch( `${getRootApiEndpoint()}/workspaces/${workspaceData.workspace_id}/users`, { method: 'POST', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(inviteData), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } ); /** * Update user permissions */ updateUserPermissions = this.withWorkspaceAuth(['write'])( async (workspaceData, userId, permissions) => { const response = await fetch( `${getRootApiEndpoint()}/workspaces/${workspaceData.workspace_id}/users/${userId}/permissions`, { method: 'PUT', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(permissions), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } ); /** * Remove user from workspace */ removeUser = this.withWorkspaceAuth(['delete'])( async (workspaceData, userId) => { const response = await fetch( `${getRootApiEndpoint()}/workspaces/${workspaceData.workspace_id}/users/${userId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${this.authService.getToken()}`, }, } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } ); // ======================================== // WORKSPACE MANAGEMENT OPERATORS // ======================================== /** * Get workspace details */ getWorkspace = this.withWorkspaceAuth(['read'])(async workspaceData => { const response = await fetch( `${getRootApiEndpoint()}/workspaces/${workspaceData.workspace_id}`, { method: 'GET', 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(); }); /** * Update workspace settings */ updateWorkspace = this.withWorkspaceAuth(['write'])( async (workspaceData, updateData) => { const response = await fetch( `${getRootApiEndpoint()}/workspaces/${workspaceData.workspace_id}`, { method: 'PUT', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(updateData), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } ); /** * Get workspace usage statistics */ getWorkspaceUsage = this.withWorkspaceAuth(['read'])(async workspaceData => { const response = await fetch( `${getRootApiEndpoint()}/workspaces/${workspaceData.workspace_id}/usage`, { method: 'GET', 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(); }); // ======================================== // SUBSCRIPTION MANAGEMENT OPERATORS // ======================================== /** * Get subscription details */ getSubscription = this.withAuth(async subscriptionId => { // Check access to subscription const canAccess = await this.authService.canAccessEntity( 'subscription', subscriptionId, ['read'] ); if (!canAccess) { throw new Error('Access denied to this subscription'); } const response = await fetch( `${getRootApiEndpoint()}/subscriptions/${subscriptionId}`, { method: 'GET', 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(); }); /** * Get user's accessible subscriptions (cost pools) */ getAccessibleSubscriptions = this.withAuth(async () => { const response = await fetch(`${getRootApiEndpoint()}/subscriptions`, { method: 'GET', 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(); }); /** * Verify subscription access and payment method status */ verifySubscriptionForPurchase = this.withAuth(async subscriptionId => { // Check access to subscription const canAccess = await this.authService.canAccessEntity( 'subscription', subscriptionId, ['read', 'write'] ); if (!canAccess) { throw new Error('Access denied to this subscription'); } const response = await fetch( `${getRootApiEndpoint()}/subscriptions/${subscriptionId}/purchase-verification`, { method: 'GET', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const verificationResult = await response.json(); // Check if subscription has active payment method if (!verificationResult.hasActivePaymentMethod) { throw new Error('Subscription does not have an active payment method'); } return verificationResult; }); /** * Update subscription */ updateSubscription = this.withAuth(async (subscriptionId, updateData) => { // Check access to subscription const canAccess = await this.authService.canAccessEntity( 'subscription', subscriptionId, ['write'] ); if (!canAccess) { throw new Error('Access denied to update this subscription'); } const response = await fetch( `${getRootApiEndpoint()}/subscriptions/${subscriptionId}`, { method: 'PUT', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(updateData), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); }); // ======================================== // PURCHASING OPERATORS // ======================================== /** * Create token record in dataverse for purchase */ createPurchaseToken = this.withAuth(async (subscriptionId, purchaseData) => { // Verify subscription access and payment method first await this.verifySubscriptionForPurchase(subscriptionId); const response = await fetch( `${getRootApiEndpoint()}/subscriptions/${subscriptionId}/tokens`, { method: 'POST', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ ...purchaseData, subscription_id: subscriptionId, created_by: this.authService.getUserPermission('contactId'), workspace_id: this.authService.getUserPermission('workspaceId'), account_id: this.authService.getUserPermission('accountId'), }), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); }); /** * Process purchase using APIM operators */ processPurchase = this.withAuth(async (subscriptionId, purchaseDetails) => { // Verify subscription access and payment method const verification = await this.verifySubscriptionForPurchase(subscriptionId); if (!verification.canPurchase) { throw new Error('Purchase not allowed for this subscription'); } // Create purchase token record in dataverse const tokenRecord = await this.createPurchaseToken(subscriptionId, { product_type: purchaseDetails.productType, quantity: purchaseDetails.quantity, amount: purchaseDetails.amount, currency: purchaseDetails.currency || 'USD', }); // Process the actual purchase const response = await fetch( `${getRootApiEndpoint()}/subscriptions/${subscriptionId}/purchase`, { method: 'POST', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ token_id: tokenRecord.id, ...purchaseDetails, }), } ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); }); /** * Get purchase history for subscription */ getPurchaseHistory = this.withAuth(async (subscriptionId, options = {}) => { // Check access to subscription const canAccess = await this.authService.canAccessEntity( 'subscription', subscriptionId, ['read'] ); if (!canAccess) { throw new Error( 'Access denied to purchase history for this subscription' ); } const queryString = new URLSearchParams(options).toString(); const response = await fetch( `${getRootApiEndpoint()}/subscriptions/${subscriptionId}/purchases?${queryString}`, { method: 'GET', 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(); }); /** * Get billing history */ getBillingHistory = this.withAuth(async (subscriptionId, options = {}) => { // Check access to subscription const canAccess = await this.authService.canAccessEntity( 'subscription', subscriptionId, ['read'] ); if (!canAccess) { throw new Error( 'Access denied to billing information for this subscription' ); } const queryString = new URLSearchParams(options).toString(); const response = await fetch( `${getRootApiEndpoint()}/subscriptions/${subscriptionId}/billing?${queryString}`, { method: 'GET', 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(); }); // ======================================== // PROFILE MANAGEMENT OPERATORS // ======================================== /** * Get user profile */ getUserProfile = this.withAuth(async () => { const contactId = this.authService.getUserPermission('contactId'); if (!contactId) { throw new Error('Contact ID not found in token'); } // Verify contact access const canAccess = await this.authService.canAccessEntity( 'contact', contactId, ['read'] ); if (!canAccess) { throw new Error('Access denied to user profile'); } const response = await fetch(`${getRootApiEndpoint()}/profile`, { method: 'GET', 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(); }); /** * Update user profile */ updateUserProfile = this.withAuth(async profileData => { const contactId = this.authService.getUserPermission('contactId'); if (!contactId) { throw new Error('Contact ID not found in token'); } // Verify contact access const canAccess = await this.authService.canAccessEntity( 'contact', contactId, ['write'] ); if (!canAccess) { throw new Error('Access denied to update user profile'); } const response = await fetch(`${getRootApiEndpoint()}/profile`, { method: 'PUT', headers: { Authorization: `Bearer ${this.authService.getToken()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(profileData), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); }); // ======================================== // SUPPORT TICKETS OPERATORS (from service) // ======================================== getSupportTickets = this.supportTicketsService.getSupportTickets.bind( this.supportTicketsService ); getSupportTicket = this.supportTicketsService.getSupportTicket.bind( this.supportTicketsService ); createSupportTicket = this.supportTicketsService.createSupportTicket.bind( this.supportTicketsService ); updateSupportTicket = this.supportTicketsService.updateSupportTicket.bind( this.supportTicketsService ); addTicketMessage = this.supportTicketsService.addTicketMessage.bind( this.supportTicketsService ); closeSupportTicket = this.supportTicketsService.closeSupportTicket.bind( this.supportTicketsService ); reopenSupportTicket = this.supportTicketsService.reopenSupportTicket.bind( this.supportTicketsService ); // ======================================== // NOTIFICATIONS OPERATORS (from service) // ======================================== getNotifications = this.notificationsService.getNotifications.bind( this.notificationsService ); createNotification = this.notificationsService.createNotification.bind( this.notificationsService ); updateNotificationStatus = this.notificationsService.updateNotificationStatus.bind( this.notificationsService ); deleteNotification = this.notificationsService.deleteNotification.bind( this.notificationsService ); markAllNotificationsAsRead = this.notificationsService.markAllAsRead.bind( this.notificationsService ); // ======================================== // CONTACT-US OPERATORS // ======================================== /** * Submit contact form (unauthenticated) */ submitContactForm = async formData => { try { // This can work for unauthenticated users const response = await fetch(`${getRootApiEndpoint()}/contact`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ...formData, source: 'website_contact_form', timestamp: new Date().toISOString(), }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Contact form submission failed:', error); throw error; } }; /** * Submit support ticket via contact form (authenticated) */ submitSupportTicketForm = this.withAuth(async formData => { // This uses the support tickets service return await this.supportTicketsService.createSupportTicket({ title: formData.subject || 'Support Request', description: formData.message, priority: this.determinePriority(formData.subject), contact_email: formData.email, contact_name: formData.name, source: 'contact_form', }); }); // ======================================== // UTILITY METHODS // ======================================== /** * Determine priority based on subject */ determinePriority(subject) { const highPriorityKeywords = [ 'urgent', 'critical', 'down', 'broken', 'error', 'billing', ]; const mediumPriorityKeywords = ['issue', 'problem', 'bug', 'help']; const subjectLower = (subject || '').toLowerCase(); if (highPriorityKeywords.some(keyword => subjectLower.includes(keyword))) { return 'high'; } if ( mediumPriorityKeywords.some(keyword => subjectLower.includes(keyword)) ) { return 'medium'; } return 'low'; } /** * Clear operator cache */ clearCache() { this.operatorCache.clear(); } /** * Generic operator call method for administrative API endpoints */ async callOperator(category, operation, data = {}) { try { // Ensure authentication if (!this.authService.isUserAuthenticated()) { throw new Error('Authentication required for this operation'); } // Build the endpoint URL based on category and operation const endpoint = this.buildOperatorEndpoint(category, operation); // Prepare the request body with auth data const requestBody = { ...data, authData: { ...this.authService.getAuthData(), ...data.authData, }, }; const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.authService.getToken()}`, }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error(`Operator ${category}/${operation} failed:`, error); throw error; } } /** * Build operator endpoint URL */ buildOperatorEndpoint(category, operation) { const baseUrl = getRootApiEndpoint(); // Map categories and operations to endpoint paths const endpointMap = { workspace_management: { list_workspaces: '/administrative/workspace/list', create_workspace: '/administrative/workspace/create', }, workspace_admin_management: { get_workspace_admins: '/administrative/workspace/get-admins', update_workspace_admins: '/administrative/workspace/update-admins', get_users_for_admin_selection: '/administrative/user/list-for-admin-selection', }, user_management: { list_users: '/administrative/user/list', invite_user: '/administrative/user/invite', reassign_user: '/administrative/user/reassign', }, bot_management: { list_bots: '/administrative/bot-config/list', // Uses existing bot config list endpoint list_bot_configs: '/administrative/bot-config/list', create_bot_config: '/administrative/bot-config/create', cancel_bot_config: '/administrative/bot-config/cancel', }, subscription_management: { // Note: No generic list endpoint exists, using payment methods as fallback list_cost_pools: '/administrative/cost-center/list-payment-methods', create_cost_center: '/administrative/cost-center/create', }, cost_pool_management: { list_cost_centers: '/administrative/cost-center/list-payment-methods', create_cost_center: '/administrative/cost-center/create', }, // Note: These endpoints need to be implemented in the function app account_management: { get_account_stats: '/administrative/workspace/list', // Fallback to workspace list for now }, dashboard: { get_kpis: '/administrative/workspace/list', // Fallback to workspace list for now get_activity: '/administrative/workspace/list', // Fallback to workspace list for now get_dashboard_data: '/administrative/workspace/list', // Fallback to workspace list for now }, }; const categoryEndpoints = endpointMap[category]; if (!categoryEndpoints) { throw new Error(`Unknown operator category: ${category}`); } const endpoint = categoryEndpoints[operation]; if (!endpoint) { throw new Error( `Unknown operation: ${operation} in category: ${category}` ); } return `${baseUrl}${endpoint}`; } /** * Cleanup resources */ destroy() { this.clearCache(); } } // Create singleton instance const pageOperatorsService = new PageOperatorsService(); export default pageOperatorsService; export { PageOperatorsService };