diff --git a/libs/auth/application/src/lib/auth.service.spec.ts b/libs/auth/application/src/lib/auth.service.spec.ts index 800ab66..a954b2d 100644 --- a/libs/auth/application/src/lib/auth.service.spec.ts +++ b/libs/auth/application/src/lib/auth.service.spec.ts @@ -15,4 +15,54 @@ describe('AuthService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('signIn', () => { + it('should return access token when credentials are valid', async () => { + const mockEmail = 'test@example.com'; + const mockPassword = 'password123'; + const mockUser = { + id: '123', + email: mockEmail, + firstName: 'John', + lastName: 'Doe', + }; + const mockAccessToken = 'mock.jwt.token'; + + jest.spyOn(service['awsCognitoService'], 'signIn').mockResolvedValue({}); + jest + .spyOn(service['usersService'], 'findByEmail') + .mockResolvedValue(mockUser); + jest + .spyOn(service['jwtService'], 'signAsync') + .mockResolvedValue(mockAccessToken); + + const result = await service.signIn(mockEmail, mockPassword); + + expect(result).toEqual({ accessToken: mockAccessToken }); + expect(service['awsCognitoService'].signIn).toHaveBeenCalledWith( + mockEmail, + mockPassword, + ); + expect(service['usersService'].findByEmail).toHaveBeenCalledWith( + mockEmail, + ); + expect(service['jwtService'].signAsync).toHaveBeenCalledWith({ + sub: mockUser.id, + username: mockUser.email, + }); + }); + + it('should throw UnauthorizedException when credentials are invalid', async () => { + const mockEmail = 'test@example.com'; + const mockPassword = 'wrongpassword'; + + jest + .spyOn(service['awsCognitoService'], 'signIn') + .mockRejectedValue(new Error('Invalid credentials')); + + await expect(service.signIn(mockEmail, mockPassword)).rejects.toThrow( + 'Unauthorized', + ); + }); + }); }); diff --git a/libs/auth/application/src/lib/auth.service.ts b/libs/auth/application/src/lib/auth.service.ts index 744d876..030caa2 100644 --- a/libs/auth/application/src/lib/auth.service.ts +++ b/libs/auth/application/src/lib/auth.service.ts @@ -1,21 +1,33 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { UsersService } from '@users/application'; +import { JwtService } from '@nestjs/jwt'; import { AwsCognitoService } from '@shared/infrastructure-aws-cognito'; export class AuthService { - constructor(private readonly awsCognitoService: AwsCognitoService) {} + constructor( + private awsCognitoService: AwsCognitoService, + private usersService: UsersService, + private jwtService: JwtService, + ) {} + async signIn( - username: string, + email: string, pass: string, ): Promise<{ accessToken: string; }> { - // TODO: Implement sign in - // Step 1: Validate user credentials via AWS Cognito - const authResponse = await this.awsCognitoService.signIn(username, pass); - // Step 2: Retrieve user from the database - // Step 3: Generate a custom JWT access token - - return { - accessToken: 'accessToken', - }; + try { + await this.awsCognitoService.signIn(email, pass); + const user = await this.usersService.findByEmail(email); + const accessToken = await this.jwtService.signAsync({ + sub: user.id, + username: user.email, + }); + return { + accessToken, + }; + } catch (error) { + throw new UnauthorizedException(error); + } } } diff --git a/libs/users/application/src/lib/users.service.ts b/libs/users/application/src/lib/users.service.ts index 8583406..83c908f 100644 --- a/libs/users/application/src/lib/users.service.ts +++ b/libs/users/application/src/lib/users.service.ts @@ -10,4 +10,9 @@ export class UsersService { async findById(id: string): Promise { return this.getUserUseCase.execute(id); } + + async findByEmail(email: string): Promise { + // TODO: refactor getUserUseCase + return this.getUserUseCase.execute(email); + } } diff --git a/package.json b/package.json index 230ac7f..0f5956e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.7", "@nestjs/graphql": "^12.2.1", + "@nestjs/jwt": "^10.2.0", "@nestjs/mongoose": "^10.1.0", "@nestjs/platform-express": "^10.4.7", "@nestjs/throttler": "^6.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4f262c..414c461 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@nestjs/graphql': specifier: ^12.2.1 version: 12.2.1(@apollo/subgraph@2.9.3(graphql@16.9.0))(@nestjs/common@10.4.12(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.12)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(ts-morph@24.0.0) + '@nestjs/jwt': + specifier: ^10.2.0 + version: 10.2.0(@nestjs/common@10.4.12(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) '@nestjs/mongoose': specifier: ^10.1.0 version: 10.1.0(@nestjs/common@10.4.12(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.12)(mongoose@8.8.3(socks@2.8.3))(rxjs@7.8.1) @@ -1431,6 +1434,11 @@ packages: ts-morph: optional: true + '@nestjs/jwt@10.2.0': + resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/mapped-types@2.0.5': resolution: {integrity: sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==} peerDependencies: @@ -2120,6 +2128,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.5': + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} @@ -2745,6 +2756,9 @@ packages: engines: {node: '>= 0.4.0'} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3380,6 +3394,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -4724,6 +4741,16 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + kareem@2.6.3: resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} engines: {node: '>=12.0.0'} @@ -4849,6 +4876,18 @@ packages: lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} @@ -4864,6 +4903,9 @@ packages: lodash.omit@4.5.0: resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -9551,6 +9593,12 @@ snapshots: - bufferutil - utf-8-validate + '@nestjs/jwt@10.2.0(@nestjs/common@10.4.12(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + dependencies: + '@nestjs/common': 10.4.12(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@types/jsonwebtoken': 9.0.5 + jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.0.5(@nestjs/common@10.4.12(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-validator@0.14.1)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 10.4.12(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -10521,6 +10569,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.5': + dependencies: + '@types/node': 22.10.1 + '@types/long@4.0.2': {} '@types/mime@1.3.5': {} @@ -11275,6 +11327,8 @@ snapshots: btoa@1.2.1: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@4.9.2: @@ -11947,6 +12001,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} ejs@3.1.10: @@ -13551,6 +13609,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.3 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + kareem@2.6.3: {} keycharm@0.2.0: {} @@ -13692,6 +13774,14 @@ snapshots: lodash.escaperegexp@4.1.2: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + lodash.isplainobject@4.0.6: {} lodash.isstring@4.0.1: {} @@ -13702,6 +13792,8 @@ snapshots: lodash.omit@4.5.0: {} + lodash.once@4.1.1: {} + lodash.sortby@4.7.0: {} lodash.uniq@4.5.0: {}