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.
This commit is contained in:
321
ts/api/handlers/package.api.ts
Normal file
321
ts/api/handlers/package.api.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 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' } };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user