Files
registry/ts/api/handlers/package.api.ts
Juergen Kunz ab88ac896f feat: implement account settings and API tokens management
- Added SettingsComponent for user profile management, including display name and password change functionality.
- Introduced TokensComponent for managing API tokens, including creation and revocation.
- Created LayoutComponent for consistent application layout with navigation and user information.
- Established main application structure in index.html and main.ts.
- Integrated Tailwind CSS for styling and responsive design.
- Configured TypeScript settings for strict type checking and module resolution.
2025-11-27 22:15:38 +00:00

322 lines
9.1 KiB
TypeScript

/**
* Package API handlers
*/
import type { IApiContext, IApiResponse } from '../router.ts';
import { PermissionService } from '../../services/permission.service.ts';
import { Package, Repository } from '../../models/index.ts';
import type { TRegistryProtocol } from '../../interfaces/auth.interfaces.ts';
export class PackageApi {
private permissionService: PermissionService;
constructor(permissionService: PermissionService) {
this.permissionService = permissionService;
}
/**
* GET /api/v1/packages (search)
*/
public async search(ctx: IApiContext): Promise<IApiResponse> {
try {
const query = ctx.url.searchParams.get('q') || '';
const protocol = ctx.url.searchParams.get('protocol') as TRegistryProtocol | undefined;
const organizationId = ctx.url.searchParams.get('organizationId') || undefined;
const limit = parseInt(ctx.url.searchParams.get('limit') || '50', 10);
const offset = parseInt(ctx.url.searchParams.get('offset') || '0', 10);
// For authenticated users, search includes private packages they have access to
// For anonymous users, only search public packages
const isPrivate = ctx.actor?.userId ? undefined : false;
const packages = await Package.search(query, {
protocol,
organizationId,
isPrivate,
limit,
offset,
});
// Filter out packages user doesn't have access to
const accessiblePackages = [];
for (const pkg of packages) {
if (!pkg.isPrivate) {
accessiblePackages.push(pkg);
continue;
}
if (ctx.actor?.userId) {
const canAccess = await this.permissionService.canAccessPackage(
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'read'
);
if (canAccess) {
accessiblePackages.push(pkg);
}
}
}
return {
status: 200,
body: {
packages: accessiblePackages.map((pkg) => ({
id: pkg.id,
name: pkg.name,
description: pkg.description,
protocol: pkg.protocol,
organizationId: pkg.organizationId,
repositoryId: pkg.repositoryId,
latestVersion: pkg.distTags['latest'],
isPrivate: pkg.isPrivate,
downloadCount: pkg.downloadCount,
updatedAt: pkg.updatedAt,
})),
total: accessiblePackages.length,
limit,
offset,
},
};
} catch (error) {
console.error('[PackageApi] Search error:', error);
return { status: 500, body: { error: 'Failed to search packages' } };
}
}
/**
* GET /api/v1/packages/:id
*/
public async get(ctx: IApiContext): Promise<IApiResponse> {
const { id } = ctx.params;
try {
const pkg = await Package.findById(decodeURIComponent(id));
if (!pkg) {
return { status: 404, body: { error: 'Package not found' } };
}
// Check access
if (pkg.isPrivate) {
if (!ctx.actor?.userId) {
return { status: 401, body: { error: 'Authentication required' } };
}
const canAccess = await this.permissionService.canAccessPackage(
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'read'
);
if (!canAccess) {
return { status: 403, body: { error: 'Access denied' } };
}
}
return {
status: 200,
body: {
id: pkg.id,
name: pkg.name,
description: pkg.description,
protocol: pkg.protocol,
organizationId: pkg.organizationId,
repositoryId: pkg.repositoryId,
distTags: pkg.distTags,
versions: Object.keys(pkg.versions),
isPrivate: pkg.isPrivate,
downloadCount: pkg.downloadCount,
starCount: pkg.starCount,
storageBytes: pkg.storageBytes,
createdAt: pkg.createdAt,
updatedAt: pkg.updatedAt,
},
};
} catch (error) {
console.error('[PackageApi] Get error:', error);
return { status: 500, body: { error: 'Failed to get package' } };
}
}
/**
* GET /api/v1/packages/:id/versions
*/
public async listVersions(ctx: IApiContext): Promise<IApiResponse> {
const { id } = ctx.params;
try {
const pkg = await Package.findById(decodeURIComponent(id));
if (!pkg) {
return { status: 404, body: { error: 'Package not found' } };
}
// Check access
if (pkg.isPrivate) {
if (!ctx.actor?.userId) {
return { status: 401, body: { error: 'Authentication required' } };
}
const canAccess = await this.permissionService.canAccessPackage(
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'read'
);
if (!canAccess) {
return { status: 403, body: { error: 'Access denied' } };
}
}
const versions = Object.entries(pkg.versions).map(([version, data]) => ({
version,
publishedAt: data.publishedAt,
size: data.size,
downloads: data.downloads,
checksum: data.checksum,
}));
return {
status: 200,
body: {
packageId: pkg.id,
packageName: pkg.name,
distTags: pkg.distTags,
versions,
},
};
} catch (error) {
console.error('[PackageApi] List versions error:', error);
return { status: 500, body: { error: 'Failed to list versions' } };
}
}
/**
* DELETE /api/v1/packages/:id
*/
public async delete(ctx: IApiContext): Promise<IApiResponse> {
if (!ctx.actor?.userId) {
return { status: 401, body: { error: 'Authentication required' } };
}
const { id } = ctx.params;
try {
const pkg = await Package.findById(decodeURIComponent(id));
if (!pkg) {
return { status: 404, body: { error: 'Package not found' } };
}
// Check delete permission
const canDelete = await this.permissionService.canAccessPackage(
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'delete'
);
if (!canDelete) {
return { status: 403, body: { error: 'Delete permission required' } };
}
// Delete the package
await pkg.delete();
// Update repository package count
const repo = await Repository.findById(pkg.repositoryId);
if (repo) {
repo.packageCount = Math.max(0, repo.packageCount - 1);
repo.storageBytes -= pkg.storageBytes;
await repo.save();
}
return {
status: 200,
body: { message: 'Package deleted successfully' },
};
} catch (error) {
console.error('[PackageApi] Delete error:', error);
return { status: 500, body: { error: 'Failed to delete package' } };
}
}
/**
* DELETE /api/v1/packages/:id/versions/:version
*/
public async deleteVersion(ctx: IApiContext): Promise<IApiResponse> {
if (!ctx.actor?.userId) {
return { status: 401, body: { error: 'Authentication required' } };
}
const { id, version } = ctx.params;
try {
const pkg = await Package.findById(decodeURIComponent(id));
if (!pkg) {
return { status: 404, body: { error: 'Package not found' } };
}
const versionData = pkg.versions[version];
if (!versionData) {
return { status: 404, body: { error: 'Version not found' } };
}
// Check delete permission
const canDelete = await this.permissionService.canAccessPackage(
ctx.actor.userId,
pkg.organizationId,
pkg.repositoryId,
'delete'
);
if (!canDelete) {
return { status: 403, body: { error: 'Delete permission required' } };
}
// Check if this is the only version
if (Object.keys(pkg.versions).length === 1) {
return {
status: 400,
body: { error: 'Cannot delete the only version. Delete the entire package instead.' },
};
}
// Remove version
const sizeReduction = versionData.size;
delete pkg.versions[version];
pkg.storageBytes -= sizeReduction;
// Update dist tags
for (const [tag, tagVersion] of Object.entries(pkg.distTags)) {
if (tagVersion === 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 {
status: 200,
body: { message: 'Version deleted successfully' },
};
} catch (error) {
console.error('[PackageApi] Delete version error:', error);
return { status: 500, body: { error: 'Failed to delete version' } };
}
}
}