import { BadRequestException, Injectable, NotFoundException, UnauthorizedException, } from '@nestjs/common'; import { createHash, randomBytes } from 'crypto'; import type { CreateOrganizationApiKeyRequest, CreateOrganizationApiKeyResponse, OrganizationApiKeyDto, } from '@mento/shared'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class ApiKeysService { constructor(private readonly prisma: PrismaService) {} async list(organizationId: string): Promise { const rows = await this.prisma.organizationApiKey.findMany({ where: { organizationId }, orderBy: { createdAt: 'desc' }, }); return rows.map((row) => this.toDto(row)); } async create( organizationId: string, dto: CreateOrganizationApiKeyRequest, ): Promise { const name = dto.name?.trim(); if (!name) throw new BadRequestException('نام کلید الزامی است'); const rawKey = `mk_${randomBytes(24).toString('hex')}`; const prefix = rawKey.slice(0, 10); const keyHash = this.hashKey(rawKey); const row = await this.prisma.organizationApiKey.create({ data: { organizationId, name, prefix, keyHash, }, }); return { ...this.toDto(row), rawKey }; } async revoke(organizationId: string, id: string): Promise { const existing = await this.prisma.organizationApiKey.findFirst({ where: { id, organizationId }, }); if (!existing) throw new NotFoundException('کلید پیدا نشد'); if (existing.revokedAt) return this.toDto(existing); const row = await this.prisma.organizationApiKey.update({ where: { id }, data: { revokedAt: new Date() }, }); return this.toDto(row); } async authenticate( rawKey: string, ): Promise<{ organizationId: string; apiKeyId: string; name: string; role: 'owner' }> { const key = rawKey.trim(); if (!key.startsWith('mk_') || key.length < 20) { throw new UnauthorizedException('کلید API نامعتبر است'); } const keyHash = this.hashKey(key); const row = await this.prisma.organizationApiKey.findFirst({ where: { keyHash, revokedAt: null }, select: { id: true, organizationId: true, name: true }, }); if (!row) throw new UnauthorizedException('کلید API نامعتبر است'); await this.prisma.organizationApiKey.update({ where: { id: row.id }, data: { lastUsedAt: new Date() }, }); return { organizationId: row.organizationId, apiKeyId: row.id, name: row.name, role: 'owner', }; } hashKey(raw: string): string { return createHash('sha256').update(raw).digest('hex'); } private toDto(row: { id: string; name: string; prefix: string; lastUsedAt: Date | null; revokedAt: Date | null; createdAt: Date; }): OrganizationApiKeyDto { return { id: row.id, name: row.name, prefix: row.prefix, lastUsedAt: row.lastUsedAt?.toISOString() ?? null, revokedAt: row.revokedAt?.toISOString() ?? null, createdAt: row.createdAt.toISOString(), }; } }