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:
293
ts/api/handlers/repository.api.ts
Normal file
293
ts/api/handlers/repository.api.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Repository API handlers
|
||||
*/
|
||||
|
||||
import type { IApiContext, IApiResponse } from '../router.ts';
|
||||
import { PermissionService } from '../../services/permission.service.ts';
|
||||
import { AuditService } from '../../services/audit.service.ts';
|
||||
import { Repository, Organization } from '../../models/index.ts';
|
||||
import type { TRegistryProtocol } from '../../interfaces/auth.interfaces.ts';
|
||||
|
||||
export class RepositoryApi {
|
||||
private permissionService: PermissionService;
|
||||
|
||||
constructor(permissionService: PermissionService) {
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/organizations/:orgId/repositories
|
||||
*/
|
||||
public async list(ctx: IApiContext): Promise<IApiResponse> {
|
||||
if (!ctx.actor?.userId) {
|
||||
return { status: 401, body: { error: 'Authentication required' } };
|
||||
}
|
||||
|
||||
const { orgId } = ctx.params;
|
||||
|
||||
try {
|
||||
// Get accessible repositories
|
||||
const repositories = await this.permissionService.getAccessibleRepositories(
|
||||
ctx.actor.userId,
|
||||
orgId
|
||||
);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
repositories: repositories.map((repo) => ({
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
displayName: repo.displayName,
|
||||
description: repo.description,
|
||||
protocols: repo.protocols,
|
||||
isPublic: repo.isPublic,
|
||||
packageCount: repo.packageCount,
|
||||
createdAt: repo.createdAt,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[RepositoryApi] List error:', error);
|
||||
return { status: 500, body: { error: 'Failed to list repositories' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/repositories/:id
|
||||
*/
|
||||
public async get(ctx: IApiContext): Promise<IApiResponse> {
|
||||
const { id } = ctx.params;
|
||||
|
||||
try {
|
||||
const repo = await Repository.findById(id);
|
||||
if (!repo) {
|
||||
return { status: 404, body: { error: 'Repository not found' } };
|
||||
}
|
||||
|
||||
// Check access
|
||||
if (!repo.isPublic && ctx.actor?.userId) {
|
||||
const permissions = await this.permissionService.resolvePermissions({
|
||||
userId: ctx.actor.userId,
|
||||
organizationId: repo.organizationId,
|
||||
repositoryId: repo.id,
|
||||
});
|
||||
|
||||
if (!permissions.canRead) {
|
||||
return { status: 403, body: { error: 'Access denied' } };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
id: repo.id,
|
||||
organizationId: repo.organizationId,
|
||||
name: repo.name,
|
||||
displayName: repo.displayName,
|
||||
description: repo.description,
|
||||
protocols: repo.protocols,
|
||||
isPublic: repo.isPublic,
|
||||
settings: repo.settings,
|
||||
packageCount: repo.packageCount,
|
||||
storageBytes: repo.storageBytes,
|
||||
createdAt: repo.createdAt,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[RepositoryApi] Get error:', error);
|
||||
return { status: 500, body: { error: 'Failed to get repository' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/organizations/:orgId/repositories
|
||||
*/
|
||||
public async create(ctx: IApiContext): Promise<IApiResponse> {
|
||||
if (!ctx.actor?.userId) {
|
||||
return { status: 401, body: { error: 'Authentication required' } };
|
||||
}
|
||||
|
||||
const { orgId } = ctx.params;
|
||||
|
||||
// Check admin permission
|
||||
const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, orgId);
|
||||
if (!canManage) {
|
||||
return { status: 403, body: { error: 'Admin access required' } };
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await ctx.request.json();
|
||||
const { name, displayName, description, protocols, isPublic, settings } = body;
|
||||
|
||||
if (!name) {
|
||||
return { status: 400, body: { error: 'Repository name is required' } };
|
||||
}
|
||||
|
||||
// Validate name format
|
||||
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: 'Name must be lowercase alphanumeric with optional hyphens' },
|
||||
};
|
||||
}
|
||||
|
||||
// Check org exists
|
||||
const org = await Organization.findById(orgId);
|
||||
if (!org) {
|
||||
return { status: 404, body: { error: 'Organization not found' } };
|
||||
}
|
||||
|
||||
// Check if name is taken in this org
|
||||
const existing = await Repository.findByName(orgId, name);
|
||||
if (existing) {
|
||||
return { status: 409, body: { error: 'Repository name already taken in this organization' } };
|
||||
}
|
||||
|
||||
// Create repository
|
||||
const repo = new Repository();
|
||||
repo.id = await Repository.getNewId();
|
||||
repo.organizationId = orgId;
|
||||
repo.name = name;
|
||||
repo.displayName = displayName || name;
|
||||
repo.description = description;
|
||||
repo.protocols = protocols || ['npm'];
|
||||
repo.isPublic = isPublic ?? false;
|
||||
repo.settings = settings || {
|
||||
allowOverwrite: false,
|
||||
immutableTags: false,
|
||||
retentionDays: 0,
|
||||
};
|
||||
repo.createdAt = new Date();
|
||||
repo.createdById = ctx.actor.userId;
|
||||
|
||||
await repo.save();
|
||||
|
||||
// Audit log
|
||||
await AuditService.withContext({
|
||||
actorId: ctx.actor.userId,
|
||||
actorType: 'user',
|
||||
actorIp: ctx.ip,
|
||||
organizationId: orgId,
|
||||
}).logRepositoryCreated(repo.id, repo.name, orgId);
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
id: repo.id,
|
||||
organizationId: repo.organizationId,
|
||||
name: repo.name,
|
||||
displayName: repo.displayName,
|
||||
description: repo.description,
|
||||
protocols: repo.protocols,
|
||||
isPublic: repo.isPublic,
|
||||
createdAt: repo.createdAt,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[RepositoryApi] Create error:', error);
|
||||
return { status: 500, body: { error: 'Failed to create repository' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/v1/repositories/:id
|
||||
*/
|
||||
public async update(ctx: IApiContext): Promise<IApiResponse> {
|
||||
if (!ctx.actor?.userId) {
|
||||
return { status: 401, body: { error: 'Authentication required' } };
|
||||
}
|
||||
|
||||
const { id } = ctx.params;
|
||||
|
||||
try {
|
||||
const repo = await Repository.findById(id);
|
||||
if (!repo) {
|
||||
return { status: 404, body: { error: 'Repository not found' } };
|
||||
}
|
||||
|
||||
// Check admin permission
|
||||
const canManage = await this.permissionService.canManageRepository(
|
||||
ctx.actor.userId,
|
||||
repo.organizationId,
|
||||
id
|
||||
);
|
||||
if (!canManage) {
|
||||
return { status: 403, body: { error: 'Admin access required' } };
|
||||
}
|
||||
|
||||
const body = await ctx.request.json();
|
||||
const { displayName, description, protocols, isPublic, settings } = body;
|
||||
|
||||
if (displayName !== undefined) repo.displayName = displayName;
|
||||
if (description !== undefined) repo.description = description;
|
||||
if (protocols !== undefined) repo.protocols = protocols;
|
||||
if (isPublic !== undefined) repo.isPublic = isPublic;
|
||||
if (settings !== undefined) repo.settings = { ...repo.settings, ...settings };
|
||||
|
||||
await repo.save();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
displayName: repo.displayName,
|
||||
description: repo.description,
|
||||
protocols: repo.protocols,
|
||||
isPublic: repo.isPublic,
|
||||
settings: repo.settings,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[RepositoryApi] Update error:', error);
|
||||
return { status: 500, body: { error: 'Failed to update repository' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/repositories/: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 repo = await Repository.findById(id);
|
||||
if (!repo) {
|
||||
return { status: 404, body: { error: 'Repository not found' } };
|
||||
}
|
||||
|
||||
// Check admin permission
|
||||
const canManage = await this.permissionService.canManageRepository(
|
||||
ctx.actor.userId,
|
||||
repo.organizationId,
|
||||
id
|
||||
);
|
||||
if (!canManage) {
|
||||
return { status: 403, body: { error: 'Admin access required' } };
|
||||
}
|
||||
|
||||
// Check for packages
|
||||
if (repo.packageCount > 0) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: 'Cannot delete repository with packages. Remove all packages first.' },
|
||||
};
|
||||
}
|
||||
|
||||
await repo.delete();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'Repository deleted successfully' },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[RepositoryApi] Delete error:', error);
|
||||
return { status: 500, body: { error: 'Failed to delete repository' } };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user