/** * Token API handlers */ import type { IApiContext, IApiResponse } from '../router.ts'; import { TokenService } from '../../services/token.service.ts'; import { PermissionService } from '../../services/permission.service.ts'; import type { ITokenScope, TRegistryProtocol } from '../../interfaces/auth.interfaces.ts'; export class TokenApi { private tokenService: TokenService; private permissionService: PermissionService; constructor(tokenService: TokenService, permissionService?: PermissionService) { this.tokenService = tokenService; this.permissionService = permissionService || new PermissionService(); } /** * GET /api/v1/tokens * Query params: * - organizationId: list org tokens (requires org admin) */ public async list(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } try { const url = new URL(ctx.request.url); const organizationId = url.searchParams.get('organizationId'); let tokens; if (organizationId) { // Check if user can manage org const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, organizationId); if (!canManage) { return { status: 403, body: { error: 'Not authorized to view organization tokens' } }; } tokens = await this.tokenService.getOrgTokens(organizationId); } else { 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, organizationId: t.organizationId, createdById: t.createdById, 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 * Body: * - name: token name * - organizationId: (optional) create org token instead of personal * - protocols: array of protocols * - scopes: array of scope objects * - expiresInDays: (optional) token expiry */ 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, organizationId, protocols, scopes, expiresInDays } = body as { name: string; organizationId?: 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' } }; } } // If creating org token, verify permission if (organizationId) { const canManage = await this.permissionService.canManageOrganization(ctx.actor.userId, organizationId); if (!canManage) { return { status: 403, body: { error: 'Not authorized to create organization tokens' } }; } } const result = await this.tokenService.createToken({ userId: ctx.actor.userId, organizationId, createdById: 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, organizationId: result.token.organizationId, 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 * Allows revoking personal tokens or org tokens (if org admin) */ public async revoke(ctx: IApiContext): Promise { if (!ctx.actor?.userId) { return { status: 401, body: { error: 'Authentication required' } }; } const { id } = ctx.params; try { // First check if it's a personal token const userTokens = await this.tokenService.getUserTokens(ctx.actor.userId); let token = userTokens.find((t) => t.id === id); if (!token) { // Check if it's an org token and user can manage org const { ApiToken } = await import('../../models/index.ts'); const anyToken = await ApiToken.getInstance({ id, isRevoked: false }); if (anyToken?.organizationId) { const canManage = await this.permissionService.canManageOrganization( ctx.actor.userId, anyToken.organizationId ); if (canManage) { token = anyToken; } } } if (!token) { 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' } }; } } }