/** * 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 } 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 { // Get accessible repositories const repositories = await this.permissionService.getAccessibleRepositories( ctx.actor.userId, orgId ); return { status: 200, body: { repositories: repositories.map((repo) => ({ id: repo.id, name: repo.name, displayName: repo.displayName, description: repo.description, protocols: repo.protocols, 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, displayName: repo.displayName, description: repo.description, protocols: repo.protocols, isPublic: repo.isPublic, settings: repo.settings, 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, displayName, description, protocols, isPublic, settings } = body; 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 hyphens' }, }; } // Check org exists const org = await Organization.findById(orgId); if (!org) { return { status: 404, body: { error: 'Organization not found' } }; } // Check if name is taken in this org const existing = await Repository.findByName(orgId, name); if (existing) { return { status: 409, body: { error: 'Repository name already taken in this organization' } }; } // Create repository const repo = new Repository(); repo.id = await Repository.getNewId(); repo.organizationId = orgId; repo.name = name; repo.displayName = displayName || name; repo.description = description; repo.protocols = protocols || ['npm']; repo.isPublic = isPublic ?? false; repo.settings = settings || { allowOverwrite: false, immutableTags: false, retentionDays: 0, }; repo.createdAt = new Date(); repo.createdById = ctx.actor.userId; await repo.save(); // 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, displayName: repo.displayName, description: repo.description, protocols: repo.protocols, 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 { displayName, description, protocols, isPublic, settings } = body; if (displayName !== undefined) repo.displayName = displayName; if (description !== undefined) repo.description = description; if (protocols !== undefined) repo.protocols = protocols; if (isPublic !== undefined) repo.isPublic = isPublic; if (settings !== undefined) repo.settings = { ...repo.settings, ...settings }; await repo.save(); return { status: 200, body: { id: repo.id, name: repo.name, displayName: repo.displayName, description: repo.description, protocols: repo.protocols, isPublic: repo.isPublic, settings: repo.settings, }, }; } 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' } }; } } }