From 54cb3f8e7ae14ba53582f9c11b890174e5fe89cb Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Fri, 6 Jun 2025 15:53:12 -0300 Subject: [PATCH] FEAT: add api key endpoint --- docsfera.json | 235 +++++++++++++++++++++ package-lock.json | 8 +- package.json | 2 +- src/app.module.ts | 4 +- src/authentication/authentication.guard.ts | 30 ++- src/main.ts | 7 +- src/modules/api-key/api-key.controller.ts | 68 ++++++ src/modules/api-key/api-key.module.ts | 18 ++ src/modules/api-key/api-key.service.ts | 50 +++++ src/modules/api-key/dto/api-key.dto.ts | 39 ++++ 10 files changed, 449 insertions(+), 12 deletions(-) create mode 100644 src/modules/api-key/api-key.controller.ts create mode 100644 src/modules/api-key/api-key.module.ts create mode 100644 src/modules/api-key/api-key.service.ts create mode 100644 src/modules/api-key/dto/api-key.dto.ts diff --git a/docsfera.json b/docsfera.json index b6e8be8..b333454 100644 --- a/docsfera.json +++ b/docsfera.json @@ -5968,6 +5968,158 @@ } ] } + }, + "/api-key": { + "post": { + "operationId": "ApiKeyController_create", + "parameters": [ + { + "name": "dadosfera-lang", + "in": "header", + "required": false, + "schema": { + "enum": [ + "pt-br", + "en-us" + ], + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyResponseDto" + } + } + } + }, + "default": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyResponseDto" + } + } + } + } + }, + "tags": [ + "ApiKey" + ], + "security": [ + { + "access-token": [] + } + ] + }, + "get": { + "operationId": "ApiKeyController_findAll", + "parameters": [ + { + "name": "dadosfera-lang", + "in": "header", + "required": false, + "schema": { + "enum": [ + "pt-br", + "en-us" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKeyBaseResponseDto" + } + } + } + } + }, + "default": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKeyBaseResponseDto" + } + } + } + } + } + }, + "tags": [ + "ApiKey" + ], + "security": [ + { + "access-token": [] + } + ] + } + }, + "/api-key/{id}": { + "delete": { + "operationId": "ApiKeyController_remove", + "parameters": [ + { + "name": "dadosfera-lang", + "in": "header", + "required": false, + "schema": { + "enum": [ + "pt-br", + "en-us" + ], + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "ApiKey" + ], + "security": [ + { + "access-token": [] + } + ] + } } }, "info": { @@ -8848,6 +9000,89 @@ "required": [ "policies" ] + }, + "CreateApiKeyDto": { + "type": "object", + "properties": { + "permissions": { + "description": "Array of permission IDs", + "type": "array", + "items": { + "type": "number" + } + } + }, + "required": [ + "permissions" + ] + }, + "CreateApiKeyResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "key_mask": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionDto" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key_mask", + "permissions", + "created_at", + "created_by", + "key" + ] + }, + "ApiKeyBaseResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "key_mask": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionDto" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by": { + "type": "string" + } + }, + "required": [ + "id", + "key_mask", + "permissions", + "created_at", + "created_by" + ] } } } diff --git a/package-lock.json b/package-lock.json index c9d852f..2add2f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@aws-sdk/client-secrets-manager": "^3.414.0", "@dadosfera/dadosfera-logs": "^1.0.0-beta.4", "@dadosfera/protospack": "2.5.3", - "@dadosfera/protospack-v2": "3.37.0-beta.23", + "@dadosfera/protospack-v2": "3.38.0-beta.1", "@grpc/grpc-js": "^1.9.3", "@grpc/proto-loader": "^0.7.9", "@nestjs/cli": "^9.5.0", @@ -1400,9 +1400,9 @@ } }, "node_modules/@dadosfera/protospack-v2": { - "version": "3.37.0-beta.23", - "resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.37.0-beta.23.tgz", - "integrity": "sha512-Dv3rODwbiubHB4u8sI5uJOSm1ETOXzlpaxHNEmzTNG2fLTIJxsaF1GlOBST3x9HTkhJC6hUJ8r77/qQu1FKPpQ==", + "version": "3.38.0-beta.1", + "resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.38.0-beta.1.tgz", + "integrity": "sha512-4+yNZMlhvEkHlxJcaeta+jtJ9owskZcg0yjKdZzBfT8PCclctgertLHmrG0+kDb3Y/na/rpPPHYULRp7AO4zDQ==", "license": "ISC", "dependencies": { "@grpc/grpc-js": "^1.9.3", diff --git a/package.json b/package.json index fd3eb0b..b59aa9c 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@aws-sdk/client-secrets-manager": "^3.414.0", "@dadosfera/dadosfera-logs": "^1.0.0-beta.4", "@dadosfera/protospack": "2.5.3", - "@dadosfera/protospack-v2": "3.37.0-beta.23", + "@dadosfera/protospack-v2": "3.38.0-beta.1", "@grpc/grpc-js": "^1.9.3", "@grpc/proto-loader": "^0.7.9", "@nestjs/cli": "^9.5.0", diff --git a/src/app.module.ts b/src/app.module.ts index fedb388..0555eaf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,6 +29,7 @@ import { CustomersModule } from './modules/customers/customers.module'; import { OpenDataModule } from './modules/open-data/open-data.module'; import { ThemeModule } from './modules/theme/theme.module'; import { NetworkPolicyModule } from './modules/network-policy/network-policy.module'; +import { ApiKeyModule } from './modules/api-key/api-key.module'; @Module({ providers: [ @@ -64,7 +65,8 @@ import { NetworkPolicyModule } from './modules/network-policy/network-policy.mod ThemeModule, //Always leave HealthModule last, so it is on the bottom of swagger HealthModule, - NetworkPolicyModule + NetworkPolicyModule, + ApiKeyModule ], }) export class AppModule {} diff --git a/src/authentication/authentication.guard.ts b/src/authentication/authentication.guard.ts index bc69f8c..0506581 100644 --- a/src/authentication/authentication.guard.ts +++ b/src/authentication/authentication.guard.ts @@ -17,6 +17,7 @@ import { import { RequestUser } from '../decorators/user.decorator'; import ErrorBuilder from '../utils/ErrorBuilder'; import ErrorCodes from '../utils/errorCodes'; +import { ApiKeyService } from 'src/modules/api-key/api-key.service'; @Injectable() export class AuthenticationGuard @@ -31,6 +32,7 @@ export class AuthenticationGuard dadosferaLogger: DadosferaLogger, private reflector: Reflector, private authClient: AuthClientService, + private apiKeyService: ApiKeyService ) { this.pems = new Map(); this.logger = dadosferaLogger.logger; @@ -48,20 +50,40 @@ export class AuthenticationGuard }); } - canActivate(ctx: ExecutionContext): boolean { + async canActivate(ctx: ExecutionContext): Promise { const authFunctions = this.reflector.getAllAndMerge< AuthenticationFunction[] >(AUTH_FUNCTION_KEY, [ctx.getClass(), ctx.getHandler()]); const mustBeAuthenticated = authFunctions.length > 0; - const request = ctx.switchToHttp().getRequest(); - const accessToken = this.validateToken(request, mustBeAuthenticated); - if (!mustBeAuthenticated) { // no need to be authenticated return true; } + const request = ctx.switchToHttp().getRequest(); + const apiKey = request.get('X-api-key'); + if (apiKey) { + const { + api_key + } = await this.apiKeyService.get(apiKey); + + request.user = { + user_id: api_key.user_id, + username: api_key.username, + permissions: api_key.permissions, + customer_id: api_key.customer_id, + customer_name: api_key.customer_name, + customer_tier: api_key.customer_tier, + customer_modules: api_key.customer_modules, + access_token: apiKey, + }; + + return true; + } + + const accessToken = this.validateToken(request, mustBeAuthenticated); + if (!accessToken) { // couldn't load valid token throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED); diff --git a/src/main.ts b/src/main.ts index 7511269..6fd8445 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,8 +26,11 @@ async function bootstrap() { }, }); app.use(helmet()); - app.use('/catalog/register-dataset', json({ limit: '10mb' })); - app.use('/catalog/register-dataset', urlencoded({ extended: true, limit: '10mb' })); + if (process.env.ENV === 'prd') { + app.use('/catalog/register-dataset', json({ limit: '10mb' })); + app.use('/catalog/register-dataset', urlencoded({ extended: true, limit: '10mb' })); + } + configureSwagger(app); await app.listen(3333); if (process.env.KILL_AFTER_START) await app.close(); diff --git a/src/modules/api-key/api-key.controller.ts b/src/modules/api-key/api-key.controller.ts new file mode 100644 index 0000000..bc2c51c --- /dev/null +++ b/src/modules/api-key/api-key.controller.ts @@ -0,0 +1,68 @@ +import { Controller, Get, Post, Body, Param, Delete, UseFilters, Inject } from '@nestjs/common'; +import { ApiKeyService } from './api-key.service'; +import { CreateApiKeyDto, CreateApiKeyResponseDto, ApiKeyBaseResponseDto } from './dto/api-key.dto'; +import { Authenticated } from 'src/decorators/authentication.decorator'; +import { ApiHeaders, ApiTags, ApiResponse } from '@nestjs/swagger'; +import { LanguageEnum } from 'src/utils/languages.enum'; +import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter'; +import { RequestUser, User } from 'src/decorators/user.decorator'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +@Controller('api-key') +@Authenticated() +@ApiTags('ApiKey') +@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }]) +@UseFilters(new GrpcToHttpExceptionFilter()) +export class ApiKeyController { + logger: DadosferaLogger; + + constructor( + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + private readonly apiKeyService: ApiKeyService, + ) { + this.logger = dadosferaLogger.logger; + } + + @Post() + @ApiResponse({ type: CreateApiKeyResponseDto }) + async create(@Body() createApiKeyDto: CreateApiKeyDto, @User() user: RequestUser): Promise { + this.logger.info('POST /api-key', { + permissions: createApiKeyDto.permissions, + method: 'create' + }); + const result = await this.apiKeyService.create(createApiKeyDto, user); + this.logger.info('POST /api-key success', { + id: result.id, + method: 'create' + }); + return result; + } + + @Get() + @ApiResponse({ type: [ApiKeyBaseResponseDto] }) + async findAll(@User() user: RequestUser): Promise { + this.logger.info('GET /api-key', { + method: 'findAll' + }); + const result = await this.apiKeyService.findAll(user); + this.logger.info('GET /api-key success', { + count: result.length, + method: 'findAll' + }); + return result; + } + + @Delete(':id') + async remove(@Param('id') id: string, @User() user: RequestUser): Promise { + this.logger.info('DELETE /api-key/:id', { + id, + method: 'remove' + }); + await this.apiKeyService.remove(id, user); + this.logger.info('DELETE /api-key/:id success', { + id, + method: 'remove' + }); + } +} diff --git a/src/modules/api-key/api-key.module.ts b/src/modules/api-key/api-key.module.ts new file mode 100644 index 0000000..1455425 --- /dev/null +++ b/src/modules/api-key/api-key.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { ApiKeyService } from './api-key.service'; +import { ApiKeyController } from './api-key.controller'; +import { ClientsModule } from '@nestjs/microservices'; +import { DucClient } from '../duc/client.config'; +import DadosferaLogger from '@dadosfera/dadosfera-logs'; + +const ducClient = new DucClient(); + +@Module({ + imports: [ + ClientsModule.register([ducClient.providerOptions]) + ], + controllers: [ApiKeyController], + providers: [ApiKeyService, DadosferaLogger], + exports: [ApiKeyService] +}) +export class ApiKeyModule {} diff --git a/src/modules/api-key/api-key.service.ts b/src/modules/api-key/api-key.service.ts new file mode 100644 index 0000000..ea96801 --- /dev/null +++ b/src/modules/api-key/api-key.service.ts @@ -0,0 +1,50 @@ +import { Injectable, Inject, OnModuleInit } from '@nestjs/common'; +import { ClientGrpc } from '@nestjs/microservices'; +import { CreateApiKeyDto, CreateApiKeyResponseDto, ApiKeyBaseResponseDto } from './dto/api-key.dto'; +import { RequestUser } from 'src/decorators/user.decorator'; +import { DucClient } from '../duc/client.config'; +import { PackTheMetadata } from '../../utils/ PackTheMetadata'; +import { ApiKeyWriteProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service'; +import { lastValueFrom } from 'rxjs'; +import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc'; + +@Injectable() +export class ApiKeyService implements OnModuleInit { + private apiKeyService: ApiKeyWriteProtoService; + + constructor( + @Inject(DucClient.name) private readonly client: ClientGrpc, + ) {} + + onModuleInit() { + this.apiKeyService = this.client.getService(ProtoServices.ApiKeyWriteProtoService); + } + + create(createApiKeyDto: CreateApiKeyDto, user: RequestUser): Promise { + const metadata = PackTheMetadata(user); + + return lastValueFrom(this.apiKeyService.CreateApiKey({ + permissions: createApiKeyDto.permissions + }, metadata)); + } + + async findAll(user: RequestUser): Promise { + const metadata = PackTheMetadata(user); + console.log(metadata) + + const data = await lastValueFrom(this.apiKeyService.ListApiKeys({}, metadata)); + return data.api_keys; + } + + async remove(id: string, user: RequestUser) { + const metadata = PackTheMetadata(user); + + await lastValueFrom(this.apiKeyService.DeleteApiKey({ id }, metadata)); + } + + async get(key: string) { + const metadata = PackTheMetadata({}); + + return await lastValueFrom(this.apiKeyService.GetApiKey({ key }, metadata)); + } +} diff --git a/src/modules/api-key/dto/api-key.dto.ts b/src/modules/api-key/dto/api-key.dto.ts new file mode 100644 index 0000000..2bbabf3 --- /dev/null +++ b/src/modules/api-key/dto/api-key.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNumber } from 'class-validator'; + +export class PermissionDto { + @ApiProperty({ type: Number }) + id: number; + + @ApiProperty({ type: String }) + name: string; +} + +export class ApiKeyBaseResponseDto { + @ApiProperty({ type: String, format: 'uuid' }) + id: string; + + @ApiProperty({ type: String }) + key_mask: string; + + @ApiProperty({ type: [PermissionDto] }) + permissions: PermissionDto[]; + + @ApiProperty({ type: String, format: 'date-time' }) + created_at: string; + + @ApiProperty({ type: String }) + created_by: string; +} + +export class CreateApiKeyResponseDto extends ApiKeyBaseResponseDto { + @ApiProperty({ type: String }) + key: string; +} + +export class CreateApiKeyDto { + @ApiProperty({ type: [Number], description: 'Array of permission IDs' }) + @IsArray() + @IsNumber({}, { each: true }) + permissions: number[]; +} \ No newline at end of file