import * as plugins from '../../plugins.ts'; import * as interfaces from '../../../ts_interfaces/index.ts'; import type { OpsServer } from '../classes.opsserver.ts'; import { requireValidIdentity } from '../helpers/guards.ts'; import { Organization, OrganizationMember, User } from '../../models/index.ts'; import { PermissionService } from '../../services/permission.service.ts'; import { AuditService } from '../../services/audit.service.ts'; export class OrganizationHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); private permissionService = new PermissionService(); constructor(private opsServerRef: OpsServer) { this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.registerHandlers(); } /** * Helper to resolve organization by ID or name */ private async resolveOrganization(idOrName: string): Promise { return idOrName.startsWith('Organization:') ? await Organization.findById(idOrName) : await Organization.findByName(idOrName); } private registerHandlers(): void { // Get Organizations this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getOrganizations', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const userId = dataArg.identity.userId; let organizations: Organization[]; if (dataArg.identity.isSystemAdmin) { organizations = await Organization.getInstances({}); } else { const memberships = await OrganizationMember.getUserOrganizations(userId); const orgs: Organization[] = []; for (const m of memberships) { const org = await Organization.findById(m.organizationId); if (org) orgs.push(org); } organizations = orgs; } return { organizations: organizations.map((org) => ({ id: org.id, name: org.name, displayName: org.displayName, description: org.description, avatarUrl: org.avatarUrl, website: org.website, isPublic: org.isPublic, memberCount: org.memberCount, plan: (org as any).plan || 'free', usedStorageBytes: org.usedStorageBytes || 0, storageQuotaBytes: (org as any).storageQuotaBytes || 0, createdAt: org.createdAt instanceof Date ? org.createdAt.toISOString() : String(org.createdAt), })), }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to list organizations'); } }, ), ); // Get Organization this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getOrganization', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const org = await this.resolveOrganization(dataArg.organizationId); if (!org) { throw new plugins.typedrequest.TypedResponseError('Organization not found'); } // Check access - public orgs visible to all, private requires membership if (!org.isPublic) { const isMember = await OrganizationMember.findMembership( org.id, dataArg.identity.userId, ); if (!isMember && !dataArg.identity.isSystemAdmin) { throw new plugins.typedrequest.TypedResponseError('Access denied'); } } const orgData: interfaces.data.IOrganizationDetail = { id: org.id, name: org.name, displayName: org.displayName, description: org.description, avatarUrl: org.avatarUrl, website: org.website, isPublic: org.isPublic, memberCount: org.memberCount, plan: (org as any).plan || 'free', usedStorageBytes: org.usedStorageBytes || 0, storageQuotaBytes: (org as any).storageQuotaBytes || 0, createdAt: org.createdAt instanceof Date ? org.createdAt.toISOString() : String(org.createdAt), }; // Include settings for admins if (dataArg.identity.isSystemAdmin && org.settings) { orgData.settings = org.settings as any; } return { organization: orgData }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to get organization'); } }, ), ); // Create Organization this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createOrganization', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const { name, displayName, description, isPublic } = dataArg; if (!name) { throw new plugins.typedrequest.TypedResponseError('Organization name is required'); } // Validate name format if (!/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/.test(name)) { throw new plugins.typedrequest.TypedResponseError( 'Name must be lowercase alphanumeric with optional hyphens and dots', ); } // Check uniqueness const existing = await Organization.findByName(name); if (existing) { throw new plugins.typedrequest.TypedResponseError('Organization name already taken'); } // Create organization const org = new Organization(); org.id = await Organization.getNewId(); org.name = name; org.displayName = displayName || name; org.description = description; org.isPublic = isPublic ?? false; org.memberCount = 1; org.createdAt = new Date(); org.createdById = dataArg.identity.userId; await org.save(); // Add creator as owner const membership = new OrganizationMember(); membership.id = await OrganizationMember.getNewId(); membership.organizationId = org.id; membership.userId = dataArg.identity.userId; membership.role = 'owner'; membership.invitedBy = dataArg.identity.userId; membership.joinedAt = new Date(); await membership.save(); // Audit log await AuditService.withContext({ actorId: dataArg.identity.userId, actorType: 'user', }).logOrganizationCreated(org.id, org.name); return { organization: { id: org.id, name: org.name, displayName: org.displayName, description: org.description, isPublic: org.isPublic, memberCount: org.memberCount, plan: (org as any).plan || 'free', usedStorageBytes: org.usedStorageBytes || 0, storageQuotaBytes: (org as any).storageQuotaBytes || 0, createdAt: org.createdAt instanceof Date ? org.createdAt.toISOString() : String(org.createdAt), }, }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to create organization'); } }, ), ); // Update Organization this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'updateOrganization', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const org = await this.resolveOrganization(dataArg.organizationId); if (!org) { throw new plugins.typedrequest.TypedResponseError('Organization not found'); } // Check admin permission const canManage = await this.permissionService.canManageOrganization( dataArg.identity.userId, org.id, ); if (!canManage) { throw new plugins.typedrequest.TypedResponseError('Admin access required'); } if (dataArg.displayName !== undefined) org.displayName = dataArg.displayName; if (dataArg.description !== undefined) org.description = dataArg.description; if (dataArg.avatarUrl !== undefined) org.avatarUrl = dataArg.avatarUrl; if (dataArg.website !== undefined) org.website = dataArg.website; if (dataArg.isPublic !== undefined) org.isPublic = dataArg.isPublic; // Only system admins can change settings if (dataArg.settings && dataArg.identity.isSystemAdmin) { org.settings = { ...org.settings, ...dataArg.settings } as any; } await org.save(); return { organization: { id: org.id, name: org.name, displayName: org.displayName, description: org.description, avatarUrl: org.avatarUrl, website: org.website, isPublic: org.isPublic, memberCount: org.memberCount, plan: (org as any).plan || 'free', usedStorageBytes: org.usedStorageBytes || 0, storageQuotaBytes: (org as any).storageQuotaBytes || 0, createdAt: org.createdAt instanceof Date ? org.createdAt.toISOString() : String(org.createdAt), }, }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to update organization'); } }, ), ); // Delete Organization this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deleteOrganization', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const org = await this.resolveOrganization(dataArg.organizationId); if (!org) { throw new plugins.typedrequest.TypedResponseError('Organization not found'); } // Only owners and system admins can delete const membership = await OrganizationMember.findMembership( org.id, dataArg.identity.userId, ); if (membership?.role !== 'owner' && !dataArg.identity.isSystemAdmin) { throw new plugins.typedrequest.TypedResponseError('Owner access required'); } await org.delete(); return { message: 'Organization deleted successfully' }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to delete organization'); } }, ), ); // Get Organization Members this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getOrganizationMembers', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const org = await this.resolveOrganization(dataArg.organizationId); if (!org) { throw new plugins.typedrequest.TypedResponseError('Organization not found'); } // Check membership const isMember = await OrganizationMember.findMembership( org.id, dataArg.identity.userId, ); if (!isMember && !dataArg.identity.isSystemAdmin) { throw new plugins.typedrequest.TypedResponseError('Access denied'); } const members = await OrganizationMember.getOrgMembers(org.id); const membersWithUsers = await Promise.all( members.map(async (m) => { const user = await User.findById(m.userId); return { userId: m.userId, role: m.role as interfaces.data.TOrganizationRole, addedAt: m.joinedAt instanceof Date ? m.joinedAt.toISOString() : String(m.joinedAt), user: user ? { username: user.username, displayName: user.displayName, avatarUrl: user.avatarUrl, } : null, }; }), ); return { members: membersWithUsers }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to list members'); } }, ), ); // Add Organization Member this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'addOrganizationMember', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const org = await this.resolveOrganization(dataArg.organizationId); if (!org) { throw new plugins.typedrequest.TypedResponseError('Organization not found'); } // Check admin permission const canManage = await this.permissionService.canManageOrganization( dataArg.identity.userId, org.id, ); if (!canManage) { throw new plugins.typedrequest.TypedResponseError('Admin access required'); } const { userId, role } = dataArg; if (!userId || !role) { throw new plugins.typedrequest.TypedResponseError('userId and role are required'); } if (!['owner', 'admin', 'member'].includes(role)) { throw new plugins.typedrequest.TypedResponseError('Invalid role'); } // Check user exists const user = await User.findById(userId); if (!user) { throw new plugins.typedrequest.TypedResponseError('User not found'); } // Check if already a member const existing = await OrganizationMember.findMembership(org.id, userId); if (existing) { throw new plugins.typedrequest.TypedResponseError('User is already a member'); } // Add member const membership = new OrganizationMember(); membership.id = await OrganizationMember.getNewId(); membership.organizationId = org.id; membership.userId = userId; membership.role = role; membership.invitedBy = dataArg.identity.userId; membership.joinedAt = new Date(); await membership.save(); // Update member count org.memberCount += 1; await org.save(); return { member: { userId: membership.userId, role: membership.role as interfaces.data.TOrganizationRole, addedAt: membership.joinedAt instanceof Date ? membership.joinedAt.toISOString() : String(membership.joinedAt), }, }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to add member'); } }, ), ); // Update Organization Member this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'updateOrganizationMember', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const org = await this.resolveOrganization(dataArg.organizationId); if (!org) { throw new plugins.typedrequest.TypedResponseError('Organization not found'); } // Check admin permission const canManage = await this.permissionService.canManageOrganization( dataArg.identity.userId, org.id, ); if (!canManage) { throw new plugins.typedrequest.TypedResponseError('Admin access required'); } const { userId, role } = dataArg; if (!role || !['owner', 'admin', 'member'].includes(role)) { throw new plugins.typedrequest.TypedResponseError('Valid role is required'); } const membership = await OrganizationMember.findMembership(org.id, userId); if (!membership) { throw new plugins.typedrequest.TypedResponseError('Member not found'); } // Cannot change last owner if (membership.role === 'owner' && role !== 'owner') { const members = await OrganizationMember.getOrgMembers(org.id); const ownerCount = members.filter((m) => m.role === 'owner').length; if (ownerCount <= 1) { throw new plugins.typedrequest.TypedResponseError('Cannot remove the last owner'); } } membership.role = role; await membership.save(); return { member: { userId: membership.userId, role: membership.role as interfaces.data.TOrganizationRole, }, }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to update member'); } }, ), ); // Remove Organization Member this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'removeOrganizationMember', async (dataArg) => { await requireValidIdentity(this.opsServerRef.authHandler, dataArg); try { const org = await this.resolveOrganization(dataArg.organizationId); if (!org) { throw new plugins.typedrequest.TypedResponseError('Organization not found'); } // Users can remove themselves, admins can remove others if (dataArg.userId !== dataArg.identity.userId) { const canManage = await this.permissionService.canManageOrganization( dataArg.identity.userId, org.id, ); if (!canManage) { throw new plugins.typedrequest.TypedResponseError('Admin access required'); } } const membership = await OrganizationMember.findMembership(org.id, dataArg.userId); if (!membership) { throw new plugins.typedrequest.TypedResponseError('Member not found'); } // Cannot remove last owner if (membership.role === 'owner') { const members = await OrganizationMember.getOrgMembers(org.id); const ownerCount = members.filter((m) => m.role === 'owner').length; if (ownerCount <= 1) { throw new plugins.typedrequest.TypedResponseError('Cannot remove the last owner'); } } await membership.delete(); // Update member count org.memberCount = Math.max(0, org.memberCount - 1); await org.save(); return { message: 'Member removed successfully' }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to remove member'); } }, ), ); } }