diff --git a/backend/src/api-keys/api-keys.controller.ts b/backend/src/api-keys/api-keys.controller.ts new file mode 100644 index 0000000..2d65beb --- /dev/null +++ b/backend/src/api-keys/api-keys.controller.ts @@ -0,0 +1,42 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Req, + UseGuards, +} from "@nestjs/common"; +import { AuthGuard } from "../auth/guards/auth.guard"; +import type { AuthenticatedRequest } from "../common/interfaces/request.interface"; +import { ApiKeysService } from "./api-keys.service"; + +@Controller("api-keys") +@UseGuards(AuthGuard) +export class ApiKeysController { + constructor(private readonly apiKeysService: ApiKeysService) {} + + @Post() + create( + @Req() req: AuthenticatedRequest, + @Body("name") name: string, + @Body("expiresAt") expiresAt?: string, + ) { + return this.apiKeysService.create( + req.user.sub, + name, + expiresAt ? new Date(expiresAt) : undefined, + ); + } + + @Get() + findAll(@Req() req: AuthenticatedRequest) { + return this.apiKeysService.findAll(req.user.sub); + } + + @Delete(":id") + revoke(@Req() req: AuthenticatedRequest, @Param("id") id: string) { + return this.apiKeysService.revoke(req.user.sub, id); + } +} diff --git a/backend/src/api-keys/api-keys.module.ts b/backend/src/api-keys/api-keys.module.ts new file mode 100644 index 0000000..cfacffc --- /dev/null +++ b/backend/src/api-keys/api-keys.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module"; +import { CryptoModule } from "../crypto/crypto.module"; +import { DatabaseModule } from "../database/database.module"; +import { ApiKeysController } from "./api-keys.controller"; +import { ApiKeysService } from "./api-keys.service"; + +@Module({ + imports: [DatabaseModule, AuthModule, CryptoModule], + controllers: [ApiKeysController], + providers: [ApiKeysService], + exports: [ApiKeysService], +}) +export class ApiKeysModule {} diff --git a/backend/src/api-keys/api-keys.service.ts b/backend/src/api-keys/api-keys.service.ts new file mode 100644 index 0000000..1a7d582 --- /dev/null +++ b/backend/src/api-keys/api-keys.service.ts @@ -0,0 +1,79 @@ +import { createHash, randomBytes } from "node:crypto"; +import { Injectable } from "@nestjs/common"; +import { and, eq } from "drizzle-orm"; +import { DatabaseService } from "../database/database.service"; +import { apiKeys } from "../database/schemas"; + +@Injectable() +export class ApiKeysService { + constructor(private readonly databaseService: DatabaseService) {} + + async create(userId: string, name: string, expiresAt?: Date) { + const prefix = "mg_live_"; + const randomPart = randomBytes(24).toString("hex"); + const key = `${prefix}${randomPart}`; + + const keyHash = createHash("sha256").update(key).digest("hex"); + + await this.databaseService.db.insert(apiKeys).values({ + userId, + name, + prefix: prefix.substring(0, 8), + keyHash, + expiresAt, + }); + + return { + name, + key, // Retourné une seule fois à la création + expiresAt, + }; + } + + async findAll(userId: string) { + return await this.databaseService.db + .select({ + id: apiKeys.id, + name: apiKeys.name, + prefix: apiKeys.prefix, + isActive: apiKeys.isActive, + lastUsedAt: apiKeys.lastUsedAt, + expiresAt: apiKeys.expiresAt, + createdAt: apiKeys.createdAt, + }) + .from(apiKeys) + .where(eq(apiKeys.userId, userId)); + } + + async revoke(userId: string, keyId: string) { + return await this.databaseService.db + .update(apiKeys) + .set({ isActive: false, updatedAt: new Date() }) + .where(and(eq(apiKeys.id, keyId), eq(apiKeys.userId, userId))) + .returning(); + } + + async validateKey(key: string) { + const keyHash = createHash("sha256").update(key).digest("hex"); + + const [apiKey] = await this.databaseService.db + .select() + .from(apiKeys) + .where(and(eq(apiKeys.keyHash, keyHash), eq(apiKeys.isActive, true))) + .limit(1); + + if (!apiKey) return null; + + if (apiKey.expiresAt && apiKey.expiresAt < new Date()) { + return null; + } + + // Update last used at + await this.databaseService.db + .update(apiKeys) + .set({ lastUsedAt: new Date() }) + .where(eq(apiKeys.id, apiKey.id)); + + return apiKey; + } +}