Files
registry/ts/opsserver/handlers/package.handler.ts

316 lines
12 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 { 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');
}
},
),
);
}
}