/** * Repository API handlers */ import type { IApiContext, IApiResponse } from '../router.ts'; import { PermissionService } from '../../services/permission.service.ts'; import { AuditService } from '../../services/audit.service.ts'; import { Repository, Organization } from '../../models/index.ts'; import type { TRegistryProtocol, TRepositoryVisibility } from '../../interfaces/auth.interfaces.ts'; export class RepositoryApi { private permissionService: PermissionService; constructor(permissionService: PermissionService) { this.permissionService = permissionService; } /** * GET /api/v1/organizations/:orgId/repositories */ public async list(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { orgId } = ctx.params; try { const repositories = await this.permissionService.getAccessibleRepositories( ctx.actor.userId, orgId ); return { status: 200, body: { repositories: repositories.map((repo) => ({ id: repo.id, name: repo.name, description: repo.description, protocol: repo.protocol, visibility: repo.visibility, isPublic: repo.isPublic, packageCount: repo.packageCount, createdAt: repo.createdAt, })), }, }; } catch (error) { console.error('[RepositoryApi] List error:', error); return { status: 500, body: { error: 'Failed to list repositories' } }; } } /** * GET /api/v1/repositories/:id */ public async get(ctx: IApiContext): Promise { const { id } = ctx.params; try { const repo = await Repository.findById(id); if (!repo) { return { status: 404, body: { error: 'Repository not found' } }; } // Check access if (!repo.isPublic && ctx.actor?.userId) { const permissions = await this.permissionService.resolvePermissions({ userId: ctx.actor.userId, organizationId: repo.organizationId, repositoryId: repo.id, }); if (!permissions.canRead) { return { status: 403, body: { error: 'Access denied' } }; } } return { status: 200, body: { id: repo.id, organizationId: repo.organizationId, name: repo.name, description: repo.description, protocol: repo.protocol, visibility: repo.visibility, isPublic: repo.isPublic, packageCount: repo.packageCount, storageBytes: repo.storageBytes, createdAt: repo.createdAt, }, }; } catch (error) { console.error('[RepositoryApi] Get error:', error); return { status: 500, body: { error: 'Failed to get repository' } }; } } /** * POST /api/v1/organizations/:orgId/repositories */ public async create(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { orgId } = ctx.params; // Check admin permission const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, orgId); if (!canManage) { return { status: 403, body: { error: 'Admin access required' } }; } try { const body = await ctx.request.json(); const { name, description, protocol, visibility } = body as { name: string; description?: string; protocol?: TRegistryProtocol; visibility?: TRepositoryVisibility; }; if (!name) { return { status: 400, body: { error: 'Repository 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 dots, hyphens, or underscores' }, }; } // Check org exists const org = await Organization.findById(orgId); if (!org) { return { status: 404, body: { error: 'Organization not found' } }; } // Create repository using the model's factory method const repo = await Repository.createRepository({ organizationId: orgId, name, description, protocol: protocol || 'npm', visibility: visibility || 'private', createdById: ctx.actor.userId, }); // Audit log await AuditService.withContext({ actorId: ctx.actor.userId, actorType: 'user', actorIp: ctx.ip, organizationId: orgId, }).logRepositoryCreated(repo.id, repo.name, orgId); return { status: 201, body: { id: repo.id, organizationId: repo.organizationId, name: repo.name, description: repo.description, protocol: repo.protocol, visibility: repo.visibility, isPublic: repo.isPublic, createdAt: repo.createdAt, }, }; } catch (error) { console.error('[RepositoryApi] Create error:', error); return { status: 500, body: { error: 'Failed to create repository' } }; } } /** * PUT /api/v1/repositories/:id */ public async update(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; try { const repo = await Repository.findById(id); if (!repo) { return { status: 404, body: { error: 'Repository not found' } }; } // Check admin permission const canManage = await this.permissionService.canManageRepository( ctx.actor.userId, repo.organizationId, id ); if (!canManage) { return { status: 403, body: { error: 'Admin access required' } }; } const body = await ctx.request.json(); const { description, visibility } = body as { description?: string; visibility?: TRepositoryVisibility; }; if (description !== undefined) repo.description = description; if (visibility !== undefined) repo.visibility = visibility; await repo.save(); return { status: 200, body: { id: repo.id, name: repo.name, description: repo.description, protocol: repo.protocol, visibility: repo.visibility, isPublic: repo.isPublic, }, }; } catch (error) { console.error('[RepositoryApi] Update error:', error); return { status: 500, body: { error: 'Failed to update repository' } }; } } /** * DELETE /api/v1/repositories/:id */ public async delete(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; try { const repo = await Repository.findById(id); if (!repo) { return { status: 404, body: { error: 'Repository not found' } }; } // Check admin permission const canManage = await this.permissionService.canManageRepository( ctx.actor.userId, repo.organizationId, id ); if (!canManage) { return { status: 403, body: { error: 'Admin access required' } }; } // Check for packages if (repo.packageCount > 0) { return { status: 400, body: { error: 'Cannot delete repository with packages. Remove all packages first.' }, }; } await repo.delete(); return { status: 200, body: { message: 'Repository deleted successfully' }, }; } catch (error) { console.error('[RepositoryApi] Delete error:', error); return { status: 500, body: { error: 'Failed to delete repository' } }; } } }