284 lines
7.9 KiB
TypeScript
284 lines
7.9 KiB
TypeScript
/**
|
|
* 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 { Organization, Repository } 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<IApiResponse> {
|
|
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<IApiResponse> {
|
|
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<IApiResponse> {
|
|
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<IApiResponse> {
|
|
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<IApiResponse> {
|
|
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' } };
|
|
}
|
|
}
|
|
}
|