feat(opsserver,web): replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend
This commit is contained in:
272
ts/opsserver/handlers/repository.handler.ts
Normal file
272
ts/opsserver/handlers/repository.handler.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
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');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user