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