feat: implement ApiKeysModule with services, controller, and CRUD operations

Added a dedicated ApiKeysModule to manage API keys. Includes functionality to create, list, revoke, and validate keys, leveraging cryptographic hashing and database support. Integrated with authentication guards for security.
This commit is contained in:
Mathis HERRIOT
2026-01-08 15:24:23 +01:00
parent 9ab737b8c7
commit 9406ed9350
3 changed files with 135 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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 {}

View File

@@ -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;
}
}