From 32997421a5cd112a03c4aff103d792319888c185 Mon Sep 17 00:00:00 2001 From: tal-rofe Date: Fri, 29 Jul 2022 18:26:39 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9E=20secrets=20are=20generated?= =?UTF-8?q?=20with=20JWT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit secrets are generated with JWT --- apps/backend/@types/global/index.d.ts | 1 + apps/backend/envs/.env.development | 3 ++- apps/backend/src/config/configuration.ts | 1 + apps/backend/src/config/env.interface.ts | 1 + apps/backend/src/config/env.validation.ts | 3 +++ .../secrets/classes/create-secret.dto.ts | 9 ++++--- .../user/modules/secrets/create.controller.ts | 15 ++++++----- .../decorators/valid-expiration.decorator.ts | 27 +++++++++++++++++++ .../contracts/create-secret.cotract.ts | 3 ++- .../queries/handlers/create-secret.handler.ts | 10 +++++-- .../user/modules/secrets/secrets.service.ts | 25 +++++++++++++---- 11 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 apps/backend/src/modules/user/modules/secrets/decorators/valid-expiration.decorator.ts diff --git a/apps/backend/@types/global/index.d.ts b/apps/backend/@types/global/index.d.ts index 8b816990c..d400d67cd 100644 --- a/apps/backend/@types/global/index.d.ts +++ b/apps/backend/@types/global/index.d.ts @@ -13,6 +13,7 @@ declare global { readonly GITHUB_OAUTH_REDIRECT_URI: string; readonly MIXPANEL_TOKEN: string; readonly FRONTEND_URL: string; + readonly CLI_TOKEN_JWT_KEY: string; } } } diff --git a/apps/backend/envs/.env.development b/apps/backend/envs/.env.development index aaa8bff52..1e3bc3c81 100644 --- a/apps/backend/envs/.env.development +++ b/apps/backend/envs/.env.development @@ -9,4 +9,5 @@ GITHUB_OAUTH_CLIENT_ID="DUMMY" GITHUB_OAUTH_CLIENT_SECRET="DUMMY" GITHUB_OAUTH_REDIRECT_URI="http://localhost:3000/user/auth/github-redirect" MIXPANEL_TOKEN="XOMBILLAH" -FRONTEND_URL="http://localhost:8080" \ No newline at end of file +FRONTEND_URL="http://localhost:8080" +CLI_TOKEN_JWT_KEY="JWT" \ No newline at end of file diff --git a/apps/backend/src/config/configuration.ts b/apps/backend/src/config/configuration.ts index f9cb0355d..4fe5fe8bc 100644 --- a/apps/backend/src/config/configuration.ts +++ b/apps/backend/src/config/configuration.ts @@ -13,6 +13,7 @@ const EnvConfiguration = (): IEnvironment => ({ githubOAuthRedirectUri: process.env.GITHUB_OAUTH_REDIRECT_URI, mixpanelToken: process.env.MIXPANEL_TOKEN, frontendUrl: process.env.FRONTEND_URL, + cliTokenJwtKey: process.env.CLI_TOKEN_JWT_KEY, }); export default EnvConfiguration; diff --git a/apps/backend/src/config/env.interface.ts b/apps/backend/src/config/env.interface.ts index e2965446b..8c5035ac7 100644 --- a/apps/backend/src/config/env.interface.ts +++ b/apps/backend/src/config/env.interface.ts @@ -11,4 +11,5 @@ export interface IEnvironment { readonly githubOAuthRedirectUri: string; readonly mixpanelToken: string; readonly frontendUrl: string; + readonly cliTokenJwtKey: string; } diff --git a/apps/backend/src/config/env.validation.ts b/apps/backend/src/config/env.validation.ts index bed493325..0f50b78d6 100644 --- a/apps/backend/src/config/env.validation.ts +++ b/apps/backend/src/config/env.validation.ts @@ -43,6 +43,9 @@ class EnvironmentVariables { @IsString() public FRONTEND_URL!: string; + + @IsString() + public CLI_TOKEN_JWT_KEY!: string; } export const validate = (config: Record) => { diff --git a/apps/backend/src/modules/user/modules/secrets/classes/create-secret.dto.ts b/apps/backend/src/modules/user/modules/secrets/classes/create-secret.dto.ts index 8b567a897..933a1875e 100644 --- a/apps/backend/src/modules/user/modules/secrets/classes/create-secret.dto.ts +++ b/apps/backend/src/modules/user/modules/secrets/classes/create-secret.dto.ts @@ -1,13 +1,16 @@ -import { IsNumber, IsString, MinLength } from 'class-validator'; +import { IsISO8601, IsString, MinLength } from 'class-validator'; import { IsNullable } from '@/decorators/is-nullable.decorator'; +import { IsValidExpiration } from '../decorators/valid-expiration.decorator'; + export class CreateSecretDto { @IsString() @MinLength(1) readonly label!: string; - @IsNumber() + @IsISO8601() + @IsValidExpiration() @IsNullable() - readonly expiration!: number | null; + readonly expiration!: string | null; } diff --git a/apps/backend/src/modules/user/modules/secrets/create.controller.ts b/apps/backend/src/modules/user/modules/secrets/create.controller.ts index fec73cb36..de803fe84 100644 --- a/apps/backend/src/modules/user/modules/secrets/create.controller.ts +++ b/apps/backend/src/modules/user/modules/secrets/create.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Logger, Post } from '@nestjs/common'; import { QueryBus } from '@nestjs/cqrs'; import { CurrentUserId } from '@/decorators/current-user-id.decorator'; +import { CurrentUserEmail } from '@/decorators/current-user-email.decorator'; import Routes from './secrets.routes'; import type { ICreateClientSecret } from './interfaces/responses'; @@ -17,18 +18,18 @@ export class CreateController { @Post(Routes.CREATE) public async create( @CurrentUserId() userId: string, + @CurrentUserEmail() userEmail: string, @Body() createSecretDto: CreateSecretDto, ): Promise { this.logger.log(`Will try to create a client secret with to user with an Id: "${userId}"`); - let expirationDate: Date | null = null; - - if (createSecretDto.expiration !== null) { - expirationDate = new Date(createSecretDto.expiration); - } - const secret = await this.queryBus.execute( - new CreateSecretContract(userId, createSecretDto.label, expirationDate), + new CreateSecretContract( + userId, + userEmail, + createSecretDto.label, + createSecretDto.expiration ? new Date(createSecretDto.expiration).getTime() : null, + ), ); this.logger.log('Successfully deleted a client secret'); diff --git a/apps/backend/src/modules/user/modules/secrets/decorators/valid-expiration.decorator.ts b/apps/backend/src/modules/user/modules/secrets/decorators/valid-expiration.decorator.ts new file mode 100644 index 000000000..6cf54242d --- /dev/null +++ b/apps/backend/src/modules/user/modules/secrets/decorators/valid-expiration.decorator.ts @@ -0,0 +1,27 @@ +import { registerDecorator } from 'class-validator'; + +export function IsValidExpiration() { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'validExpiration', + target: object.constructor, + propertyName: propertyName, + validator: { + validate(value: unknown) { + if (typeof value !== 'string') { + return false; + } + + const currentDate = new Date(); + const inputDate = new Date(value); + + if (isNaN(inputDate.getTime())) { + return false; + } + + return inputDate >= currentDate; + }, + }, + }); + }; +} diff --git a/apps/backend/src/modules/user/modules/secrets/queries/contracts/create-secret.cotract.ts b/apps/backend/src/modules/user/modules/secrets/queries/contracts/create-secret.cotract.ts index 5985943d8..b97d00754 100644 --- a/apps/backend/src/modules/user/modules/secrets/queries/contracts/create-secret.cotract.ts +++ b/apps/backend/src/modules/user/modules/secrets/queries/contracts/create-secret.cotract.ts @@ -1,7 +1,8 @@ export class CreateSecretContract { constructor( public readonly userId: string, + public readonly email: string, public readonly label: string, - public readonly expiration: Date | null, + public readonly expiration: number | null, ) {} } diff --git a/apps/backend/src/modules/user/modules/secrets/queries/handlers/create-secret.handler.ts b/apps/backend/src/modules/user/modules/secrets/queries/handlers/create-secret.handler.ts index a1cb2ad0c..a6db8c5a3 100644 --- a/apps/backend/src/modules/user/modules/secrets/queries/handlers/create-secret.handler.ts +++ b/apps/backend/src/modules/user/modules/secrets/queries/handlers/create-secret.handler.ts @@ -13,13 +13,19 @@ export class CreateSecretHandler implements IQueryHandler ) {} async execute(contract: CreateSecretContract) { - const secret = this.secretsService.generateSecret(); + const secret = await this.secretsService.generateSecret( + contract.userId, + contract.email, + contract.expiration, + ); + + const expirationDate = contract.expiration ? new Date(contract.expiration) : null; await this.dbClientSecretService.createSecret( contract.userId, secret, contract.label, - contract.expiration, + expirationDate, ); return secret; diff --git a/apps/backend/src/modules/user/modules/secrets/secrets.service.ts b/apps/backend/src/modules/user/modules/secrets/secrets.service.ts index e34961de8..9f3c92024 100644 --- a/apps/backend/src/modules/user/modules/secrets/secrets.service.ts +++ b/apps/backend/src/modules/user/modules/secrets/secrets.service.ts @@ -1,12 +1,27 @@ -import crypto from 'crypto'; - import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; + +import type { IEnvironment } from '@/config/env.interface'; @Injectable() export class SecretsService { - public generateSecret() { - const secret = crypto.randomUUID(); + constructor( + private readonly configService: ConfigService, + private readonly jwtService: JwtService, + ) {} + + public async generateSecret(userId: string, email: string, expiration: number | null) { + const jwtPayload = { + sub: userId, + email, + }; + + const token = await this.jwtService.signAsync(jwtPayload, { + secret: this.configService.get('cliTokenJwtKey', { infer: true }), + ...(expiration ? { expiresIn: expiration } : {}), + }); - return secret; + return token; } }