/** * Organization API handlers */ import type { IApiContext, IApiResponse } from '../router.ts'; import { PermissionService } from '../../services/permission.service.ts'; import { AuditService } from '../../services/audit.service.ts'; import { Organization, OrganizationMember, User } from '../../models/index.ts'; import type { TOrganizationRole } from '../../interfaces/auth.interfaces.ts'; export class OrganizationApi { private permissionService: PermissionService; constructor(permissionService: PermissionService) { this.permissionService = permissionService; } /** * GET /api/v1/organizations */ public async list(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } try { // System admins see all orgs, others see only their orgs let organizations: Organization[]; if (ctx.actor.user?.isSystemAdmin) { organizations = await Organization.getInstances({}); } else { organizations = await OrganizationMember.getUserOrganizations(ctx.actor.userId); } return { status: 200, body: { organizations: organizations.map((org) => ({ id: org.id, name: org.name, displayName: org.displayName, description: org.description, avatarUrl: org.avatarUrl, isPublic: org.isPublic, memberCount: org.memberCount, createdAt: org.createdAt, })), }, }; } catch (error) { console.error('[OrganizationApi] List error:', error); return { status: 500, body: { error: 'Failed to list organizations' } }; } } /** * GET /api/v1/organizations/:id */ public async get(ctx: IApiContext): Promise { const { id } = ctx.params; try { const org = await Organization.findById(id); if (!org) { return { status: 404, body: { error: 'Organization not found' } }; } // Check access - public orgs are visible to all authenticated users if (!org.isPublic && ctx.actor?.userId) { const isMember = await OrganizationMember.findMembership(id, ctx.actor.userId); if (!isMember && !ctx.actor.user?.isSystemAdmin) { return { status: 403, body: { error: 'Access denied' } }; } } return { status: 200, body: { id: org.id, name: org.name, displayName: org.displayName, description: org.description, avatarUrl: org.avatarUrl, website: org.website, isPublic: org.isPublic, memberCount: org.memberCount, settings: ctx.actor?.user?.isSystemAdmin ? org.settings : undefined, usedStorageBytes: org.usedStorageBytes, createdAt: org.createdAt, }, }; } catch (error) { console.error('[OrganizationApi] Get error:', error); return { status: 500, body: { error: 'Failed to get organization' } }; } } /** * POST /api/v1/organizations */ public async create(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } try { const body = await ctx.request.json(); const { name, displayName, description, isPublic } = body; if (!name) { return { status: 400, body: { error: 'Organization name is required' } }; } // Validate name format if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) { return { status: 400, body: { error: 'Name must be lowercase alphanumeric with optional hyphens' }, }; } // Check if name is taken const existing = await Organization.findByName(name); if (existing) { return { status: 409, body: { error: '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 = ctx.actor.userId; await org.save(); // Add creator as owner const membership = new OrganizationMember(); membership.id = await OrganizationMember.getNewId(); membership.organizationId = org.id; membership.userId = ctx.actor.userId; membership.role = 'owner'; membership.addedById = ctx.actor.userId; membership.addedAt = new Date(); await membership.save(); // Audit log await AuditService.withContext({ actorId: ctx.actor.userId, actorType: 'user', actorIp: ctx.ip, }).logOrganizationCreated(org.id, org.name); return { status: 201, body: { id: org.id, name: org.name, displayName: org.displayName, description: org.description, isPublic: org.isPublic, createdAt: org.createdAt, }, }; } catch (error) { console.error('[OrganizationApi] Create error:', error); return { status: 500, body: { error: 'Failed to create organization' } }; } } /** * PUT /api/v1/organizations/:id */ public async update(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; // Check admin permission const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, id); if (!canManage) { return { status: 403, body: { error: 'Admin access required' } }; } try { const org = await Organization.findById(id); if (!org) { return { status: 404, body: { error: 'Organization not found' } }; } const body = await ctx.request.json(); const { displayName, description, avatarUrl, website, isPublic, settings } = body; if (displayName !== undefined) org.displayName = displayName; if (description !== undefined) org.description = description; if (avatarUrl !== undefined) org.avatarUrl = avatarUrl; if (website !== undefined) org.website = website; if (isPublic !== undefined) org.isPublic = isPublic; // Only system admins can change settings if (settings && ctx.actor.user?.isSystemAdmin) { org.settings = { ...org.settings, ...settings }; } await org.save(); return { status: 200, body: { id: org.id, name: org.name, displayName: org.displayName, description: org.description, avatarUrl: org.avatarUrl, website: org.website, isPublic: org.isPublic, }, }; } catch (error) { console.error('[OrganizationApi] Update error:', error); return { status: 500, body: { error: 'Failed to update organization' } }; } } /** * DELETE /api/v1/organizations/:id */ public async delete(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; // Only owners and system admins can delete const membership = await OrganizationMember.findMembership(id, ctx.actor.userId); if (membership?.role !== 'owner' && !ctx.actor.user?.isSystemAdmin) { return { status: 403, body: { error: 'Owner access required' } }; } try { const org = await Organization.findById(id); if (!org) { return { status: 404, body: { error: 'Organization not found' } }; } // TODO: Check for packages, repositories before deletion // For now, just delete the organization and memberships await org.delete(); return { status: 200, body: { message: 'Organization deleted successfully' }, }; } catch (error) { console.error('[OrganizationApi] Delete error:', error); return { status: 500, body: { error: 'Failed to delete organization' } }; } } /** * GET /api/v1/organizations/:id/members */ public async listMembers(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; // Check membership const isMember = await OrganizationMember.findMembership(id, ctx.actor.userId); if (!isMember && !ctx.actor.user?.isSystemAdmin) { return { status: 403, body: { error: 'Access denied' } }; } try { const members = await OrganizationMember.getOrgMembers(id); // Fetch user details const membersWithUsers = await Promise.all( members.map(async (m) => { const user = await User.findById(m.userId); return { userId: m.userId, role: m.role, addedAt: m.addedAt, user: user ? { username: user.username, displayName: user.displayName, avatarUrl: user.avatarUrl, } : null, }; }) ); return { status: 200, body: { members: membersWithUsers }, }; } catch (error) { console.error('[OrganizationApi] List members error:', error); return { status: 500, body: { error: 'Failed to list members' } }; } } /** * POST /api/v1/organizations/:id/members */ public async addMember(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; // Check admin permission const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, id); if (!canManage) { return { status: 403, body: { error: 'Admin access required' } }; } try { const body = await ctx.request.json(); const { userId, role } = body as { userId: string; role: TOrganizationRole }; if (!userId || !role) { return { status: 400, body: { error: 'userId and role are required' } }; } if (!['owner', 'admin', 'member'].includes(role)) { return { status: 400, body: { error: 'Invalid role' } }; } // Check user exists const user = await User.findById(userId); if (!user) { return { status: 404, body: { error: 'User not found' } }; } // Check if already a member const existing = await OrganizationMember.findMembership(id, userId); if (existing) { return { status: 409, body: { error: 'User is already a member' } }; } // Add member const membership = new OrganizationMember(); membership.id = await OrganizationMember.getNewId(); membership.organizationId = id; membership.userId = userId; membership.role = role; membership.addedById = ctx.actor.userId; membership.addedAt = new Date(); await membership.save(); // Update member count const org = await Organization.findById(id); if (org) { org.memberCount += 1; await org.save(); } return { status: 201, body: { userId: membership.userId, role: membership.role, addedAt: membership.addedAt, }, }; } catch (error) { console.error('[OrganizationApi] Add member error:', error); return { status: 500, body: { error: 'Failed to add member' } }; } } /** * PUT /api/v1/organizations/:id/members/:userId */ public async updateMember(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id, userId } = ctx.params; // Check admin permission const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, id); if (!canManage) { return { status: 403, body: { error: 'Admin access required' } }; } try { const body = await ctx.request.json(); const { role } = body as { role: TOrganizationRole }; if (!role || !['owner', 'admin', 'member'].includes(role)) { return { status: 400, body: { error: 'Valid role is required' } }; } const membership = await OrganizationMember.findMembership(id, userId); if (!membership) { return { status: 404, body: { error: 'Member not found' } }; } // Cannot change last owner if (membership.role === 'owner' && role !== 'owner') { const owners = await OrganizationMember.getOrgMembers(id); const ownerCount = owners.filter((m) => m.role === 'owner').length; if (ownerCount <= 1) { return { status: 400, body: { error: 'Cannot remove the last owner' } }; } } membership.role = role; await membership.save(); return { status: 200, body: { userId: membership.userId, role: membership.role, }, }; } catch (error) { console.error('[OrganizationApi] Update member error:', error); return { status: 500, body: { error: 'Failed to update member' } }; } } /** * DELETE /api/v1/organizations/:id/members/:userId */ public async removeMember(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id, userId } = ctx.params; // Users can remove themselves, admins can remove others if (userId !== ctx.actor.userId) { const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, id); if (!canManage) { return { status: 403, body: { error: 'Admin access required' } }; } } try { const membership = await OrganizationMember.findMembership(id, userId); if (!membership) { return { status: 404, body: { error: 'Member not found' } }; } // Cannot remove last owner if (membership.role === 'owner') { const owners = await OrganizationMember.getOrgMembers(id); const ownerCount = owners.filter((m) => m.role === 'owner').length; if (ownerCount <= 1) { return { status: 400, body: { error: 'Cannot remove the last owner' } }; } } await membership.delete(); // Update member count const org = await Organization.findById(id); if (org) { org.memberCount = Math.max(0, org.memberCount - 1); await org.save(); } return { status: 200, body: { message: 'Member removed successfully' }, }; } catch (error) { console.error('[OrganizationApi] Remove member error:', error); return { status: 500, body: { error: 'Failed to remove member' } }; } } }