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:
42
backend/src/api-keys/api-keys.controller.ts
Normal file
42
backend/src/api-keys/api-keys.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
backend/src/api-keys/api-keys.module.ts
Normal file
14
backend/src/api-keys/api-keys.module.ts
Normal 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 {}
|
||||||
79
backend/src/api-keys/api-keys.service.ts
Normal file
79
backend/src/api-keys/api-keys.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user