273 lines
10 KiB
TypeScript
273 lines
10 KiB
TypeScript
|
|
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, Repository } from '../../models/index.ts';
|
||
|
|
import { PermissionService } from '../../services/permission.service.ts';
|
||
|
|
import { AuditService } from '../../services/audit.service.ts';
|
||
|
|
|
||
|
|
export class RepositoryHandler {
|
||
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||
|
|
private permissionService = new PermissionService();
|
||
|
|
|
||
|
|
constructor(private opsServerRef: OpsServer) {
|
||
|
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||
|
|
this.registerHandlers();
|
||
|
|
}
|
||
|
|
|
||
|
|
private registerHandlers(): void {
|
||
|
|
// Get Repositories
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRepositories>(
|
||
|
|
'getRepositories',
|
||
|
|
async (dataArg) => {
|
||
|
|
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const repositories = await this.permissionService.getAccessibleRepositories(
|
||
|
|
dataArg.identity.userId,
|
||
|
|
dataArg.organizationId,
|
||
|
|
);
|
||
|
|
|
||
|
|
return {
|
||
|
|
repositories: repositories.map((repo) => ({
|
||
|
|
id: repo.id,
|
||
|
|
organizationId: repo.organizationId,
|
||
|
|
name: repo.name,
|
||
|
|
description: repo.description,
|
||
|
|
protocol: repo.protocol as interfaces.data.TRegistryProtocol,
|
||
|
|
visibility: repo.visibility as interfaces.data.TRepositoryVisibility,
|
||
|
|
isPublic: repo.isPublic,
|
||
|
|
packageCount: repo.packageCount,
|
||
|
|
storageBytes: repo.storageBytes || 0,
|
||
|
|
downloadCount: (repo as any).downloadCount || 0,
|
||
|
|
createdAt: repo.createdAt instanceof Date ? repo.createdAt.toISOString() : String(repo.createdAt),
|
||
|
|
})),
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Failed to list repositories');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Get Repository
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRepository>(
|
||
|
|
'getRepository',
|
||
|
|
async (dataArg) => {
|
||
|
|
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const repo = await Repository.findById(dataArg.repositoryId);
|
||
|
|
if (!repo) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Repository not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check access
|
||
|
|
if (!repo.isPublic) {
|
||
|
|
const permissions = await this.permissionService.resolvePermissions({
|
||
|
|
userId: dataArg.identity.userId,
|
||
|
|
organizationId: repo.organizationId,
|
||
|
|
repositoryId: repo.id,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!permissions.canRead) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Access denied');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
repository: {
|
||
|
|
id: repo.id,
|
||
|
|
organizationId: repo.organizationId,
|
||
|
|
name: repo.name,
|
||
|
|
description: repo.description,
|
||
|
|
protocol: repo.protocol as interfaces.data.TRegistryProtocol,
|
||
|
|
visibility: repo.visibility as interfaces.data.TRepositoryVisibility,
|
||
|
|
isPublic: repo.isPublic,
|
||
|
|
packageCount: repo.packageCount,
|
||
|
|
storageBytes: repo.storageBytes || 0,
|
||
|
|
downloadCount: (repo as any).downloadCount || 0,
|
||
|
|
createdAt: repo.createdAt instanceof Date ? repo.createdAt.toISOString() : String(repo.createdAt),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Failed to get repository');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Create Repository
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRepository>(
|
||
|
|
'createRepository',
|
||
|
|
async (dataArg) => {
|
||
|
|
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const { organizationId, name, description, protocol, visibility } = dataArg;
|
||
|
|
|
||
|
|
// Check admin permission
|
||
|
|
const canManage = await this.permissionService.canManageOrganization(
|
||
|
|
dataArg.identity.userId,
|
||
|
|
organizationId,
|
||
|
|
);
|
||
|
|
if (!canManage) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Admin access required');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!name) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Repository 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 dots, hyphens, or underscores',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check org exists
|
||
|
|
const org = await Organization.findById(organizationId);
|
||
|
|
if (!org) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Organization not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create repository
|
||
|
|
const repo = await Repository.createRepository({
|
||
|
|
organizationId,
|
||
|
|
name,
|
||
|
|
description,
|
||
|
|
protocol: protocol || 'npm',
|
||
|
|
visibility: visibility || 'private',
|
||
|
|
createdById: dataArg.identity.userId,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Audit log
|
||
|
|
await AuditService.withContext({
|
||
|
|
actorId: dataArg.identity.userId,
|
||
|
|
actorType: 'user',
|
||
|
|
organizationId,
|
||
|
|
}).logRepositoryCreated(repo.id, repo.name, organizationId);
|
||
|
|
|
||
|
|
return {
|
||
|
|
repository: {
|
||
|
|
id: repo.id,
|
||
|
|
organizationId: repo.organizationId,
|
||
|
|
name: repo.name,
|
||
|
|
description: repo.description,
|
||
|
|
protocol: repo.protocol as interfaces.data.TRegistryProtocol,
|
||
|
|
visibility: repo.visibility as interfaces.data.TRepositoryVisibility,
|
||
|
|
isPublic: repo.isPublic,
|
||
|
|
packageCount: repo.packageCount,
|
||
|
|
storageBytes: repo.storageBytes || 0,
|
||
|
|
downloadCount: 0,
|
||
|
|
createdAt: repo.createdAt instanceof Date ? repo.createdAt.toISOString() : String(repo.createdAt),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Failed to create repository');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Update Repository
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRepository>(
|
||
|
|
'updateRepository',
|
||
|
|
async (dataArg) => {
|
||
|
|
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const repo = await Repository.findById(dataArg.repositoryId);
|
||
|
|
if (!repo) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Repository not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check admin permission
|
||
|
|
const canManage = await this.permissionService.canManageRepository(
|
||
|
|
dataArg.identity.userId,
|
||
|
|
repo.organizationId,
|
||
|
|
dataArg.repositoryId,
|
||
|
|
);
|
||
|
|
if (!canManage) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Admin access required');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (dataArg.description !== undefined) repo.description = dataArg.description;
|
||
|
|
if (dataArg.visibility !== undefined) repo.visibility = dataArg.visibility as any;
|
||
|
|
|
||
|
|
await repo.save();
|
||
|
|
|
||
|
|
return {
|
||
|
|
repository: {
|
||
|
|
id: repo.id,
|
||
|
|
organizationId: repo.organizationId,
|
||
|
|
name: repo.name,
|
||
|
|
description: repo.description,
|
||
|
|
protocol: repo.protocol as interfaces.data.TRegistryProtocol,
|
||
|
|
visibility: repo.visibility as interfaces.data.TRepositoryVisibility,
|
||
|
|
isPublic: repo.isPublic,
|
||
|
|
packageCount: repo.packageCount,
|
||
|
|
storageBytes: repo.storageBytes || 0,
|
||
|
|
downloadCount: (repo as any).downloadCount || 0,
|
||
|
|
createdAt: repo.createdAt instanceof Date ? repo.createdAt.toISOString() : String(repo.createdAt),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Failed to update repository');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Delete Repository
|
||
|
|
this.typedrouter.addTypedHandler(
|
||
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRepository>(
|
||
|
|
'deleteRepository',
|
||
|
|
async (dataArg) => {
|
||
|
|
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const repo = await Repository.findById(dataArg.repositoryId);
|
||
|
|
if (!repo) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Repository not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check admin permission
|
||
|
|
const canManage = await this.permissionService.canManageRepository(
|
||
|
|
dataArg.identity.userId,
|
||
|
|
repo.organizationId,
|
||
|
|
dataArg.repositoryId,
|
||
|
|
);
|
||
|
|
if (!canManage) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Admin access required');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for packages
|
||
|
|
if (repo.packageCount > 0) {
|
||
|
|
throw new plugins.typedrequest.TypedResponseError(
|
||
|
|
'Cannot delete repository with packages. Remove all packages first.',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
await repo.delete();
|
||
|
|
|
||
|
|
return { message: 'Repository deleted successfully' };
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||
|
|
throw new plugins.typedrequest.TypedResponseError('Failed to delete repository');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|