/** * Token API handlers */ import type { IApiContext, IApiResponse } from '../router.ts'; import { TokenService } from '../../services/token.service.ts'; import type { ITokenScope, TRegistryProtocol } from '../../interfaces/auth.interfaces.ts'; export class TokenApi { private tokenService: TokenService; constructor(tokenService: TokenService) { this.tokenService = tokenService; } /** * GET /api/v1/tokens */ public async list(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } try { const tokens = await this.tokenService.getUserTokens(ctx.actor.userId); return { status: 200, body: { tokens: tokens.map((t) => ({ id: t.id, name: t.name, tokenPrefix: t.tokenPrefix, protocols: t.protocols, scopes: t.scopes, expiresAt: t.expiresAt, lastUsedAt: t.lastUsedAt, usageCount: t.usageCount, createdAt: t.createdAt, })), }, }; } catch (error) { console.error('[TokenApi] List error:', error); return { status: 500, body: { error: 'Failed to list tokens' } }; } } /** * POST /api/v1/tokens */ public async create(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } try { const body = await ctx.request.json(); const { name, protocols, scopes, expiresInDays } = body as { name: string; protocols: TRegistryProtocol[]; scopes: ITokenScope[]; expiresInDays?: number; }; if (!name) { return { status: 400, body: { error: 'Token name is required' } }; } if (!protocols || protocols.length === 0) { return { status: 400, body: { error: 'At least one protocol is required' } }; } if (!scopes || scopes.length === 0) { return { status: 400, body: { error: 'At least one scope is required' } }; } // Validate protocols const validProtocols = ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems', '*']; for (const protocol of protocols) { if (!validProtocols.includes(protocol)) { return { status: 400, body: { error: `Invalid protocol: ${protocol}` } }; } } // Validate scopes for (const scope of scopes) { if (!scope.protocol || !scope.actions || scope.actions.length === 0) { return { status: 400, body: { error: 'Invalid scope configuration' } }; } } const result = await this.tokenService.createToken({ userId: ctx.actor.userId, name, protocols, scopes, expiresInDays, createdIp: ctx.ip, }); return { status: 201, body: { id: result.token.id, name: result.token.name, token: result.rawToken, // Only returned once! tokenPrefix: result.token.tokenPrefix, protocols: result.token.protocols, scopes: result.token.scopes, expiresAt: result.token.expiresAt, createdAt: result.token.createdAt, warning: 'Store this token securely. It will not be shown again.', }, }; } catch (error) { console.error('[TokenApi] Create error:', error); return { status: 500, body: { error: 'Failed to create token' } }; } } /** * DELETE /api/v1/tokens/:id */ public async revoke(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; try { // Get the token to verify ownership const tokens = await this.tokenService.getUserTokens(ctx.actor.userId); const token = tokens.find((t) => t.id === id); if (!token) { // Either doesn't exist or doesn't belong to user return { status: 404, body: { error: 'Token not found' } }; } const success = await this.tokenService.revokeToken(id, 'user_revoked'); if (!success) { return { status: 500, body: { error: 'Failed to revoke token' } }; } return { status: 200, body: { message: 'Token revoked successfully' }, }; } catch (error) { console.error('[TokenApi] Revoke error:', error); return { status: 500, body: { error: 'Failed to revoke token' } }; } } }