import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Inject, Param, Post, UseGuards, } from '@nestjs/common'; import { randomBytes, createHash } from 'node:crypto'; import { eq, type Db, adminTokens } from '@mosaic/db'; import { v4 as uuid } from 'uuid'; import { DB } from '../database/database.module.js'; import { AdminGuard } from './admin.guard.js'; import { CurrentUser } from '../auth/current-user.decorator.js'; import type { CreateTokenDto, TokenCreatedDto, TokenDto, TokenListDto, } from './admin-tokens.dto.js'; function hashToken(plaintext: string): string { return createHash('sha256').update(plaintext).digest('hex'); } function toTokenDto(row: typeof adminTokens.$inferSelect): TokenDto { return { id: row.id, label: row.label, scope: row.scope, expiresAt: row.expiresAt?.toISOString() ?? null, lastUsedAt: row.lastUsedAt?.toISOString() ?? null, createdAt: row.createdAt.toISOString(), }; } @Controller('api/admin/tokens') @UseGuards(AdminGuard) export class AdminTokensController { constructor(@Inject(DB) private readonly db: Db) {} @Post() async create( @Body() dto: CreateTokenDto, @CurrentUser() user: { id: string }, ): Promise { const plaintext = randomBytes(32).toString('hex'); const tokenHash = hashToken(plaintext); const id = uuid(); const expiresAt = dto.expiresInDays ? new Date(Date.now() + dto.expiresInDays * 24 * 60 * 60 * 1000) : null; const [row] = await this.db .insert(adminTokens) .values({ id, userId: user.id, tokenHash, label: dto.label ?? 'CLI token', scope: dto.scope ?? 'admin', expiresAt, }) .returning(); return { ...toTokenDto(row!), plaintext }; } @Get() async list(@CurrentUser() user: { id: string }): Promise { const rows = await this.db .select() .from(adminTokens) .where(eq(adminTokens.userId, user.id)) .orderBy(adminTokens.createdAt); return { tokens: rows.map(toTokenDto), total: rows.length }; } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async revoke(@Param('id') id: string, @CurrentUser() _user: { id: string }): Promise { await this.db.delete(adminTokens).where(eq(adminTokens.id, id)); } }