import { UserInvite, InviteUserOptions, InviteTemplate, BulkInviteResult, InvitePreview, InviteAssignment } from '../types'; import { getAuthInstance } from './auth.service'; class InviteService { private invites: Map = new Map(); private templates: Map = new Map(); private inviteExpirationMs = 7 * 24 * 60 * 60 * 1000; // 7 days default // Invite a single user async inviteUser(options: InviteUserOptions | string, role?: string, department?: string): Promise { let inviteOptions: InviteUserOptions; // Support simple invocation if (typeof options === 'string') { inviteOptions = { email: options, role, department, sendEmail: true, expiresIn: '7d', }; } else { inviteOptions = options; } // Validate email if (!this.isValidEmail(inviteOptions.email)) { throw new Error('Invalid email address'); } // Check if user already invited const existingInvite = this.findInviteByEmail(inviteOptions.email); if (existingInvite && existingInvite.status === 'pending') { throw new Error('User already has a pending invitation'); } // Create invite const invite: UserInvite = { id: this.generateInviteId(), email: inviteOptions.email, status: 'pending', assignments: { immediate: inviteOptions.immediate || { role: inviteOptions.role, department: inviteOptions.department, permissions: inviteOptions.permissions, }, conditional: inviteOptions.conditional, deferred: inviteOptions.deferred, }, invitedBy: await this.getCurrentUserId(), invitedAt: new Date(), expiresAt: this.calculateExpirationDate(inviteOptions.expiresIn || '7d'), metadata: inviteOptions.metadata, }; // Store invite this.invites.set(invite.id, invite); // Send email if requested if (inviteOptions.sendEmail !== false) { await this.sendInviteEmail(invite); } return invite; } // Bulk invite users async inviteUsers(invites: InviteUserOptions[]): Promise { const result: BulkInviteResult = { successful: [], failed: [], }; for (const inviteOptions of invites) { try { const invite = await this.inviteUser(inviteOptions); result.successful.push(invite.email); } catch (error: any) { result.failed.push({ email: inviteOptions.email, error: error.message, }); } } return result; } // Define an invite template async defineInviteTemplate(id: string, template: Omit): Promise { const fullTemplate: InviteTemplate = { ...template, id, createdBy: await this.getCurrentUserId(), createdAt: new Date(), }; this.templates.set(id, fullTemplate); } // Invite user with template async inviteUserWithTemplate(templateId: string, overrides: Partial): Promise { const template = this.templates.get(templateId); if (!template) { throw new Error(`Template ${templateId} not found`); } const inviteOptions: InviteUserOptions = { ...template.assignments, ...overrides, }; return this.inviteUser(inviteOptions); } // Preview permissions for an invite previewPermissions(options: { role?: string; department?: string }): InvitePreview { // This would typically fetch from your permission configuration const rolePermissions = this.getPermissionsForRole(options.role); const departmentPermissions = this.getPermissionsForDepartment(options.department); const allPermissions = new Set([ ...rolePermissions, ...departmentPermissions, ]); return { fromRole: rolePermissions, fromDepartment: departmentPermissions, explicit: [], total: Array.from(allPermissions), }; } // Accept an invite async acceptInvite(inviteId: string): Promise { const invite = this.invites.get(inviteId); if (!invite) { throw new Error('Invite not found'); } if (invite.status !== 'pending') { throw new Error(`Invite is ${invite.status}`); } if (new Date() > invite.expiresAt) { invite.status = 'expired'; throw new Error('Invite has expired'); } // Apply immediate assignments if (invite.assignments.immediate) { await this.applyAssignments(invite.assignments.immediate); } // Check conditional assignments if (invite.assignments.conditional) { const shouldApply = await this.checkConditionalAssignment(invite.assignments.conditional); if (shouldApply && invite.assignments.conditional.thenAssign) { await this.applyAssignments(invite.assignments.conditional.thenAssign); } } // Mark invite as accepted invite.status = 'accepted'; invite.acceptedAt = new Date(); // Trigger callback if configured const auth = getAuthInstance(); const config = (auth as any).config; if (config.onInviteAccepted) { const user = auth.getCurrentUser(); if (user) { await config.onInviteAccepted(user, invite); } } } // Cancel an invite async cancelInvite(inviteId: string): Promise { const invite = this.invites.get(inviteId); if (!invite) { throw new Error('Invite not found'); } if (invite.status !== 'pending') { throw new Error('Can only cancel pending invites'); } invite.status = 'cancelled'; } // Get invite by ID getInvite(inviteId: string): UserInvite | null { return this.invites.get(inviteId) || null; } // Get invite by email getInviteByEmail(email: string): UserInvite | null { return this.findInviteByEmail(email); } // Get all invites getAllInvites(filter?: { status?: string; invitedBy?: string }): UserInvite[] { let invites = Array.from(this.invites.values()); if (filter?.status) { invites = invites.filter(i => i.status === filter.status); } if (filter?.invitedBy) { invites = invites.filter(i => i.invitedBy === filter.invitedBy); } return invites; } // Get invite permissions async getInvitePermissions(inviteId: string): Promise { const invite = this.invites.get(inviteId); if (!invite) { throw new Error('Invite not found'); } const permissions = new Set(); // Add immediate permissions if (invite.assignments.immediate) { const { role, department, permissions: explicitPerms } = invite.assignments.immediate; if (role) { this.getPermissionsForRole(role).forEach(p => permissions.add(p)); } if (department) { this.getPermissionsForDepartment(department).forEach(p => permissions.add(p)); } if (explicitPerms) { explicitPerms.forEach(p => permissions.add(p)); } } return Array.from(permissions); } // Private helper methods private isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } private generateInviteId(): string { return `inv_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } private findInviteByEmail(email: string): UserInvite | null { for (const invite of this.invites.values()) { if (invite.email === email) { return invite; } } return null; } private calculateExpirationDate(expiresIn: string): Date { const now = new Date(); const match = expiresIn.match(/^(\d+)([hdwmy])$/); if (!match) { return new Date(now.getTime() + this.inviteExpirationMs); } const [, value, unit] = match; const num = parseInt(value, 10); switch (unit) { case 'h': return new Date(now.getTime() + num * 60 * 60 * 1000); case 'd': return new Date(now.getTime() + num * 24 * 60 * 60 * 1000); case 'w': return new Date(now.getTime() + num * 7 * 24 * 60 * 60 * 1000); case 'm': return new Date(now.getTime() + num * 30 * 24 * 60 * 60 * 1000); case 'y': return new Date(now.getTime() + num * 365 * 24 * 60 * 60 * 1000); default: return new Date(now.getTime() + this.inviteExpirationMs); } } private async getCurrentUserId(): Promise { const auth = getAuthInstance(); const user = auth.getCurrentUser(); return user?.uid || 'system'; } private async sendInviteEmail(invite: UserInvite): Promise { // This would integrate with the email service console.log(`Sending invite email to ${invite.email}`); } private async applyAssignments(assignments: InviteAssignment): Promise { // This would typically call your backend API to set custom claims console.log('Applying assignments:', assignments); } private async checkConditionalAssignment(conditional: any): Promise { // Check conditions like email domain if (conditional.ifEmailDomain) { const auth = getAuthInstance(); const user = auth.getCurrentUser(); if (user?.email?.endsWith(`@${conditional.ifEmailDomain}`)) { return true; } } return false; } private getPermissionsForRole(role?: string): string[] { // This would fetch from your role configuration const rolePermissions: Record = { admin: ['manage:users', 'manage:settings', 'edit:content', 'view:content'], editor: ['edit:content', 'publish:content', 'view:content'], viewer: ['view:content'], }; return role ? (rolePermissions[role] || []) : []; } private getPermissionsForDepartment(department?: string): string[] { // This would fetch from your department configuration const departmentPermissions: Record = { marketing: ['edit:campaigns', 'view:analytics'], engineering: ['edit:code', 'deploy:apps'], }; return department ? (departmentPermissions[department] || []) : []; } } // Singleton instance let inviteInstance: InviteService | null = null; // Get invite service instance export function getInviteService(): InviteService { if (!inviteInstance) { inviteInstance = new InviteService(); } return inviteInstance; }