diff --git a/api/src/modules/auth/authentication/authentication.controller.ts b/api/src/modules/auth/authentication/authentication.controller.ts index d3f36520..cda10c04 100644 --- a/api/src/modules/auth/authentication/authentication.controller.ts +++ b/api/src/modules/auth/authentication/authentication.controller.ts @@ -58,9 +58,23 @@ export class AuthenticationController { ): Promise { return tsRestHandler( authContract.requestPasswordRecovery, - async ({ body }) => { - const { email } = body; + async ({ body: { email } }) => { await this.passwordRecovery.requestPasswordRecovery(email, origin); + return { + body: null, + status: HttpStatus.CREATED, + }; + }, + ); + } + + @Public() + @TsRestHandler(authContract.validateToken) + async validateToken(): Promise { + return tsRestHandler( + authContract.validateToken, + async ({ headers: { authorization }, query: { tokenType } }) => { + await this.authService.verifyToken(authorization, tokenType); return { body: null, status: HttpStatus.OK, diff --git a/api/src/modules/auth/authentication/authentication.service.ts b/api/src/modules/auth/authentication/authentication.service.ts index 1b66103d..9f3379df 100644 --- a/api/src/modules/auth/authentication/authentication.service.ts +++ b/api/src/modules/auth/authentication/authentication.service.ts @@ -8,12 +8,15 @@ import { JwtPayload } from '@api/modules/auth/strategies/jwt.strategy'; import { EventBus } from '@nestjs/cqrs'; import { UserSignedUpEvent } from '@api/modules/events/user-events/user-signed-up.event'; import { UserWithAccessToken } from '@shared/dtos/user.dto'; +import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; +import { ApiConfigService } from '@api/modules/config/app-config.service'; @Injectable() export class AuthenticationService { constructor( private readonly usersService: UsersService, private readonly jwt: JwtService, + private readonly apiConfig: ApiConfigService, private readonly eventBus: EventBus, ) {} async validateUser(email: string, password: string): Promise { @@ -24,6 +27,7 @@ export class AuthenticationService { throw new UnauthorizedException(`Invalid credentials`); } + // TODO: Move logic to createUser service async signUp(signupDto: LoginDto): Promise { const passwordHash = await bcrypt.hash(signupDto.password, 10); const newUser = await this.usersService.createUser({ @@ -38,4 +42,14 @@ export class AuthenticationService { const accessToken: string = this.jwt.sign(payload); return { user, accessToken }; } + + async verifyToken(token: string, type: TOKEN_TYPE_ENUM): Promise { + const { secret } = this.apiConfig.getJWTConfigByType(type); + try { + this.jwt.verify(token, { secret }); + return true; + } catch (error) { + return false; + } + } } diff --git a/api/src/modules/auth/services/password-recovery.service.ts b/api/src/modules/auth/services/password-recovery.service.ts index 2ac355a8..fd637ed2 100644 --- a/api/src/modules/auth/services/password-recovery.service.ts +++ b/api/src/modules/auth/services/password-recovery.service.ts @@ -6,6 +6,7 @@ import { EventBus } from '@nestjs/cqrs'; import { PasswordRecoveryRequestedEvent } from '@api/modules/events/user-events/password-recovery-requested.event'; import { ApiConfigService } from '@api/modules/config/app-config.service'; import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; +import { User } from '@shared/entities/users/user.entity'; @Injectable() export class PasswordRecoveryService { @@ -18,7 +19,7 @@ export class PasswordRecoveryService { private readonly apiConfig: ApiConfigService, ) {} - async recoverPassword(email: string, origin: string): Promise { + async requestPasswordRecovery(email: string, origin: string): Promise { const user = await this.users.findByEmail(email); if (!user) { this.logger.warn( @@ -39,7 +40,7 @@ export class PasswordRecoveryService { this.eventBus.publish(new PasswordRecoveryRequestedEvent(email, user.id)); } - async resetPassword(token: string, password: string): Promise { + async resetPassword(user: User): Promise { throw new NotImplementedException(); } } diff --git a/api/test/e2e/features/password-recovery.feature b/api/test/e2e/features/password-recovery-send-email.feature similarity index 100% rename from api/test/e2e/features/password-recovery.feature rename to api/test/e2e/features/password-recovery-send-email.feature diff --git a/api/test/e2e/steps/password-recovery.steps.ts b/api/test/e2e/steps/password-recovery.steps.ts index d1b30932..33a60dcf 100644 --- a/api/test/e2e/steps/password-recovery.steps.ts +++ b/api/test/e2e/steps/password-recovery.steps.ts @@ -5,7 +5,9 @@ import { IEmailServiceToken } from '@api/modules/notifications/email/email-servi import { MockEmailService } from 'api/test/utils/mocks/mock-email.service'; import { TestManager } from 'api/test/utils/test-manager'; -const feature = loadFeature('./test/e2e/features/password-recovery.feature'); +const feature = loadFeature( + './test/e2e/features/password-recovery-send-email.feature', +); defineFeature(feature, (test) => { let testManager: TestManager; diff --git a/shared/config/.env.test b/shared/config/.env.test index d3e99aa9..00f13d4b 100644 --- a/shared/config/.env.test +++ b/shared/config/.env.test @@ -3,8 +3,21 @@ DB_PORT=5432 DB_NAME=blc DB_USERNAME=blue-carbon-cost DB_PASSWORD=blue-carbon-cost -JWT_SECRET=mysecret -JWT_EXPIRES_IN=1d + + +# Access Token Configuration +ACCESS_TOKEN_SECRET=access_token_secret +ACCESS_TOKEN_EXPIRES_IN=2h + +# Reset Password Token Configuration +RESET_PASSWORD_TOKEN_SECRET=reset_password_token_secret +RESET_PASSWORD_TOKEN_EXPIRES_IN=2h + +# Email Confirmation Token Configuration +EMAIL_CONFIRMATION_TOKEN_SECRET=email_confirmation_token_secret +EMAIL_CONFIRMATION_TOKEN_EXPIRES_IN=2h + + AWS_SES_ACCESS_KEY_ID=test AWS_SES_ACCESS_KEY_SECRET=test AWS_SES_DOMAIN=test diff --git a/shared/contracts/auth/auth.contract.ts b/shared/contracts/auth/auth.contract.ts index f75798ea..d7dc6a49 100644 --- a/shared/contracts/auth/auth.contract.ts +++ b/shared/contracts/auth/auth.contract.ts @@ -21,6 +21,7 @@ export const authContract = contract.router({ validateToken: { method: "GET", path: "/authentication/validate-token", + headers: z.object({ authorization: z.string() }), responses: { 200: null, 401: null, diff --git a/shared/schemas/auth/token-type.schema.ts b/shared/schemas/auth/token-type.schema.ts new file mode 100644 index 00000000..fda0a032 --- /dev/null +++ b/shared/schemas/auth/token-type.schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export enum TOKEN_TYPE_ENUM { + ACCESS = "access", + RESET_PASSWORD = "reset-password", + EMAIL_CONFIRMATION = "email-confirmation", +} + +export const TokenTypeSchema = z.object({ + tokenType: z.nativeEnum(TOKEN_TYPE_ENUM), +});