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:
315
ts/opsserver/handlers/package.handler.ts
Normal file
315
ts/opsserver/handlers/package.handler.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
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 { Package, Repository } from '../../models/index.ts';
|
||||
import { PermissionService } from '../../services/permission.service.ts';
|
||||
|
||||
export class PackageHandler {
|
||||
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 {
|
||||
// Search Packages
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SearchPackages>(
|
||||
'searchPackages',
|
||||
async (dataArg) => {
|
||||
try {
|
||||
const query = dataArg.query || '';
|
||||
const protocol = dataArg.protocol;
|
||||
const organizationId = dataArg.organizationId;
|
||||
const limit = dataArg.limit || 50;
|
||||
const offset = dataArg.offset || 0;
|
||||
|
||||
// Determine visibility: anonymous users see only public packages
|
||||
const hasIdentity = !!dataArg.identity?.jwt;
|
||||
const isPrivate = hasIdentity ? undefined : false;
|
||||
|
||||
const packages = await Package.searchPackages(query, {
|
||||
protocol,
|
||||
organizationId,
|
||||
isPrivate,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
|
||||
// Filter out packages user doesn't have access to
|
||||
const accessiblePackages: typeof packages = [];
|
||||
for (const pkg of packages) {
|
||||
if (!pkg.isPrivate) {
|
||||
accessiblePackages.push(pkg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasIdentity && dataArg.identity) {
|
||||
const canAccess = await this.permissionService.canAccessPackage(
|
||||
dataArg.identity.userId,
|
||||
pkg.organizationId,
|
||||
pkg.repositoryId,
|
||||
'read',
|
||||
);
|
||||
if (canAccess) {
|
||||
accessiblePackages.push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
packages: accessiblePackages.map((pkg) => ({
|
||||
id: pkg.id,
|
||||
name: pkg.name,
|
||||
description: pkg.description,
|
||||
protocol: pkg.protocol as interfaces.data.TRegistryProtocol,
|
||||
organizationId: pkg.organizationId,
|
||||
repositoryId: pkg.repositoryId,
|
||||
latestVersion: pkg.distTags?.['latest'],
|
||||
isPrivate: pkg.isPrivate,
|
||||
downloadCount: pkg.downloadCount || 0,
|
||||
starCount: pkg.starCount || 0,
|
||||
storageBytes: pkg.storageBytes || 0,
|
||||
updatedAt: pkg.updatedAt instanceof Date ? pkg.updatedAt.toISOString() : String(pkg.updatedAt),
|
||||
createdAt: pkg.createdAt instanceof Date ? pkg.createdAt.toISOString() : String(pkg.createdAt),
|
||||
})),
|
||||
total: accessiblePackages.length,
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to search packages');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get Package
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPackage>(
|
||||
'getPackage',
|
||||
async (dataArg) => {
|
||||
try {
|
||||
const pkg = await Package.findById(dataArg.packageId);
|
||||
if (!pkg) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Package not found');
|
||||
}
|
||||
|
||||
// Check access for private packages
|
||||
if (pkg.isPrivate) {
|
||||
if (!dataArg.identity?.jwt) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Authentication required');
|
||||
}
|
||||
|
||||
const canAccess = await this.permissionService.canAccessPackage(
|
||||
dataArg.identity.userId,
|
||||
pkg.organizationId,
|
||||
pkg.repositoryId,
|
||||
'read',
|
||||
);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Access denied');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
package: {
|
||||
id: pkg.id,
|
||||
name: pkg.name,
|
||||
description: pkg.description,
|
||||
protocol: pkg.protocol as interfaces.data.TRegistryProtocol,
|
||||
organizationId: pkg.organizationId,
|
||||
repositoryId: pkg.repositoryId,
|
||||
latestVersion: pkg.distTags?.['latest'],
|
||||
isPrivate: pkg.isPrivate,
|
||||
downloadCount: pkg.downloadCount || 0,
|
||||
starCount: pkg.starCount || 0,
|
||||
storageBytes: pkg.storageBytes || 0,
|
||||
distTags: pkg.distTags || {},
|
||||
versions: Object.keys(pkg.versions || {}),
|
||||
updatedAt: pkg.updatedAt instanceof Date ? pkg.updatedAt.toISOString() : String(pkg.updatedAt),
|
||||
createdAt: pkg.createdAt instanceof Date ? pkg.createdAt.toISOString() : String(pkg.createdAt),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to get package');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get Package Versions
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPackageVersions>(
|
||||
'getPackageVersions',
|
||||
async (dataArg) => {
|
||||
try {
|
||||
const pkg = await Package.findById(dataArg.packageId);
|
||||
if (!pkg) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Package not found');
|
||||
}
|
||||
|
||||
// Check access for private packages
|
||||
if (pkg.isPrivate) {
|
||||
if (!dataArg.identity?.jwt) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Authentication required');
|
||||
}
|
||||
|
||||
const canAccess = await this.permissionService.canAccessPackage(
|
||||
dataArg.identity.userId,
|
||||
pkg.organizationId,
|
||||
pkg.repositoryId,
|
||||
'read',
|
||||
);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Access denied');
|
||||
}
|
||||
}
|
||||
|
||||
const versions = Object.entries(pkg.versions || {}).map(([version, data]) => ({
|
||||
version,
|
||||
publishedAt: data.publishedAt instanceof Date ? data.publishedAt.toISOString() : String(data.publishedAt || ''),
|
||||
size: data.size || 0,
|
||||
downloads: data.downloads || 0,
|
||||
checksum: data.metadata?.checksum as interfaces.data.IPackageVersion['checksum'],
|
||||
}));
|
||||
|
||||
return {
|
||||
packageId: pkg.id,
|
||||
packageName: pkg.name,
|
||||
distTags: pkg.distTags || {},
|
||||
versions,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to list versions');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Delete Package
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeletePackage>(
|
||||
'deletePackage',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const pkg = await Package.findById(dataArg.packageId);
|
||||
if (!pkg) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Package not found');
|
||||
}
|
||||
|
||||
// Check delete permission
|
||||
const canDelete = await this.permissionService.canAccessPackage(
|
||||
dataArg.identity.userId,
|
||||
pkg.organizationId,
|
||||
pkg.repositoryId,
|
||||
'delete',
|
||||
);
|
||||
|
||||
if (!canDelete) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Delete permission required');
|
||||
}
|
||||
|
||||
// Update repository counts before deleting
|
||||
const repo = await Repository.findById(pkg.repositoryId);
|
||||
if (repo) {
|
||||
repo.packageCount = Math.max(0, repo.packageCount - 1);
|
||||
repo.storageBytes -= pkg.storageBytes;
|
||||
await repo.save();
|
||||
}
|
||||
|
||||
await pkg.delete();
|
||||
|
||||
return { message: 'Package deleted successfully' };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to delete package');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Delete Package Version
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeletePackageVersion>(
|
||||
'deletePackageVersion',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const pkg = await Package.findById(dataArg.packageId);
|
||||
if (!pkg) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Package not found');
|
||||
}
|
||||
|
||||
const versionData = pkg.versions?.[dataArg.version];
|
||||
if (!versionData) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Version not found');
|
||||
}
|
||||
|
||||
// Check delete permission
|
||||
const canDelete = await this.permissionService.canAccessPackage(
|
||||
dataArg.identity.userId,
|
||||
pkg.organizationId,
|
||||
pkg.repositoryId,
|
||||
'delete',
|
||||
);
|
||||
|
||||
if (!canDelete) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Delete permission required');
|
||||
}
|
||||
|
||||
// Check if this is the only version
|
||||
if (Object.keys(pkg.versions).length === 1) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Cannot delete the only version. Delete the entire package instead.',
|
||||
);
|
||||
}
|
||||
|
||||
// Remove version
|
||||
const sizeReduction = versionData.size || 0;
|
||||
delete pkg.versions[dataArg.version];
|
||||
pkg.storageBytes -= sizeReduction;
|
||||
|
||||
// Update dist tags
|
||||
for (const [tag, tagVersion] of Object.entries(pkg.distTags || {})) {
|
||||
if (tagVersion === dataArg.version) {
|
||||
delete pkg.distTags[tag];
|
||||
}
|
||||
}
|
||||
|
||||
// Set new latest if needed
|
||||
if (!pkg.distTags['latest'] && Object.keys(pkg.versions).length > 0) {
|
||||
const versions = Object.keys(pkg.versions).sort();
|
||||
pkg.distTags['latest'] = versions[versions.length - 1];
|
||||
}
|
||||
|
||||
await pkg.save();
|
||||
|
||||
// Update repository storage
|
||||
const repo = await Repository.findById(pkg.repositoryId);
|
||||
if (repo) {
|
||||
repo.storageBytes -= sizeReduction;
|
||||
await repo.save();
|
||||
}
|
||||
|
||||
return { message: 'Version deleted successfully' };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to delete version');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user