diff --git a/api/package.json b/api/package.json index 8013a477..5accf928 100644 --- a/api/package.json +++ b/api/package.json @@ -25,6 +25,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.2", + "@ts-rest/nest": "^3.51.0", "bcrypt": "catalog:", "class-transformer": "catalog:", "lodash": "^4.17.21", diff --git a/api/src/modules/auth/authentication/authentication.controller.ts b/api/src/modules/auth/authentication/authentication.controller.ts index f8b3f103..eeb014fd 100644 --- a/api/src/modules/auth/authentication/authentication.controller.ts +++ b/api/src/modules/auth/authentication/authentication.controller.ts @@ -1,34 +1,46 @@ -import { Body, Controller, Post, UseGuards, Headers } from '@nestjs/common'; +import { + Body, + Controller, + Post, + UseGuards, + Headers, + UseInterceptors, + ClassSerializerInterceptor, +} from '@nestjs/common'; import { User } from '@shared/entities/users/user.entity'; import { AuthenticationService } from '@api/modules/auth/authentication/authentication.service'; -import { LoginDto } from '@api/modules/auth/dtos/login.dto'; import { LocalAuthGuard } from '@api/modules/auth/guards/local-auth.guard'; import { GetUser } from '@api/modules/auth/decorators/get-user.decorator'; import { Public } from '@api/modules/auth/decorators/is-public.decorator'; import { PasswordRecoveryService } from '@api/modules/auth/services/password-recovery.service'; +import { authContract } from '@shared/contracts/auth/auth.contract'; +import { tsRestHandler, TsRestHandler } from '@ts-rest/nest'; +import { ControllerResponse } from '@api/types/controller-response.type'; -@Controller('authentication') +@Controller() +@UseInterceptors(ClassSerializerInterceptor) export class AuthenticationController { constructor( private authService: AuthenticationService, private readonly passwordRecovery: PasswordRecoveryService, ) {} - @Public() - @Post('signup') - async signup(@Body() signupDto: LoginDto) { - return this.authService.signUp(signupDto); - } - @Public() @UseGuards(LocalAuthGuard) - @Post('login') - async login(@GetUser() user: User) { - return this.authService.logIn(user); + @TsRestHandler(authContract.login) + async login(@GetUser() user: User): Promise { + return tsRestHandler(authContract.login, async () => { + const userWithAccessToken = await this.authService.logIn(user); + return { + body: userWithAccessToken, + status: 201, + }; + }); } + // TODO: Wrap this in a ts-rest handler @Public() - @Post('recover-password') + @Post('authentication/recover-password') async recoverPassword( @Headers('origin') origin: string, @Body() body: { email: string }, diff --git a/api/src/modules/auth/authentication/authentication.service.ts b/api/src/modules/auth/authentication/authentication.service.ts index 6392a865..1b66103d 100644 --- a/api/src/modules/auth/authentication/authentication.service.ts +++ b/api/src/modules/auth/authentication/authentication.service.ts @@ -7,6 +7,7 @@ import { LoginDto } from '@api/modules/auth/dtos/login.dto'; 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'; @Injectable() export class AuthenticationService { @@ -32,7 +33,7 @@ export class AuthenticationService { this.eventBus.publish(new UserSignedUpEvent(newUser.id, newUser.email)); } - async logIn(user: User): Promise<{ user: User; accessToken: string }> { + async logIn(user: User): Promise { const payload: JwtPayload = { id: user.id }; const accessToken: string = this.jwt.sign(payload); return { user, accessToken }; diff --git a/api/src/types/controller-response.type.ts b/api/src/types/controller-response.type.ts new file mode 100644 index 00000000..f512f24d --- /dev/null +++ b/api/src/types/controller-response.type.ts @@ -0,0 +1,5 @@ +/** + * Due to the inability to import TsRest return type, we need to esxplixitly define the return type of the route handler + */ + +export type ControllerResponse = unknown; diff --git a/api/test/e2e/features/sign-up.feature b/api/test/e2e/features/sign-up.feature deleted file mode 100644 index 6743e93d..00000000 --- a/api/test/e2e/features/sign-up.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: User Sign Up - - Scenario: A user cannot sign up with an already registered email - Given a user exists with valid credentials - When the user attempts to sign up with the same email - Then the user should receive a 409 status code - And the response message should be "Email already exists" - - Scenario: A user successfully signs up with a new email - When a user attempts to sign up with valid credentials - Then the user should be registered successfully - And the user should have a valid ID and email diff --git a/api/test/e2e/steps/sign-up.steps.ts b/api/test/e2e/steps/sign-up.steps.ts deleted file mode 100644 index b125d7ce..00000000 --- a/api/test/e2e/steps/sign-up.steps.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { defineFeature, loadFeature } from 'jest-cucumber'; -import { Response } from 'supertest'; -import { User } from '@shared/entities/users/user.entity'; -import { TestManager } from 'api/test/utils/test-manager'; - -const feature = loadFeature('./test/e2e/features/sign-up.feature'); - -defineFeature(feature, (test) => { - let testManager: TestManager; - - beforeAll(async () => { - testManager = await TestManager.createTestManager(); - }); - - beforeEach(async () => { - await testManager.clearDatabase(); - }); - - afterAll(async () => { - await testManager.close(); - }); - - test('A user cannot sign up with an already registered email', ({ - given, - when, - then, - and, - }) => { - let existingUser: User; - let response: Response; - - given('a user exists with valid credentials', async () => { - existingUser = await testManager.mocks().createUser({ - email: 'existing@test.com', - password: 'password123', - }); - }); - - when('the user attempts to sign up with the same email', async () => { - response = await testManager - .request() - .post('/authentication/signup') - .send({ email: existingUser.email, password: 'password123' }); - }); - - then( - /the user should receive a (\d+) status code/, - async (statusCode: string) => { - expect(response.status).toBe(Number.parseInt(statusCode)); - }, - ); - - and('the response message should be "Email already exists"', async () => { - expect(response.body.message).toEqual( - `Email ${existingUser.email} already exists`, - ); - }); - }); - - test('A user successfully signs up with a new email', ({ - when, - then, - and, - }) => { - let newUser: { email: string; password: string }; - let createdUser: User; - let response: Response; - - when('a user attempts to sign up with valid credentials', async () => { - newUser = { email: 'newuser@test.com', password: '12345678' }; - response = await testManager - .request() - .post('/authentication/signup') - .send({ - email: newUser.email, - password: newUser.password, - }); - }); - - then('the user should be registered successfully', async () => { - expect(response.status).toBe(201); - }); - - and('the user should have a valid ID and email', async () => { - createdUser = await testManager - .getDataSource() - .getRepository(User) - .findOne({ where: { email: newUser.email } }); - - expect(createdUser.id).toBeDefined(); - expect(createdUser.email).toEqual(newUser.email); - }); - }); -}); diff --git a/client/lib/queryClient.ts b/client/lib/queryClient.ts new file mode 100644 index 00000000..4dea80de --- /dev/null +++ b/client/lib/queryClient.ts @@ -0,0 +1,10 @@ +import { router } from "@shared/contracts"; +import { initQueryClient } from "@ts-rest/react-query"; + +// TODO: We need to get the baseUrl from the environment, pending to decide where to store this data. Right now the API +// is getting all the conf from the shared folder + +export const client = initQueryClient(router, { + validateResponse: true, + baseUrl: "localhost:4000", +}); diff --git a/client/package.json b/client/package.json index 5045e3e5..dba993e2 100644 --- a/client/package.json +++ b/client/package.json @@ -9,18 +9,19 @@ "lint": "next lint" }, "dependencies": { + "@ts-rest/react-query": "^3.51.0", + "next": "14.2.8", "react": "^18", - "react-dom": "^18", - "next": "14.2.8" + "react-dom": "^18" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.8", "postcss": "^8", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.8" + "typescript": "^5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36cac8a8..da8c5325 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,15 +12,9 @@ catalogs: class-transformer: specifier: 0.5.1 version: 0.5.1 - pg: - specifier: 8.12.0 - version: 8.12.0 typeorm: specifier: 0.3.20 version: 0.3.20 - zod: - specifier: 3.23.8 - version: 3.23.8 importers: @@ -55,6 +49,9 @@ importers: '@nestjs/typeorm': specifier: ^10.0.2 version: 10.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(pg@8.12.0)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4))) + '@ts-rest/nest': + specifier: ^3.51.0 + version: 3.51.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@ts-rest/core@3.51.0(@types/node@20.16.5)(zod@3.23.8))(rxjs@7.8.1)(zod@3.23.8) bcrypt: specifier: 'catalog:' version: 5.1.1 @@ -173,6 +170,9 @@ importers: client: dependencies: + '@ts-rest/react-query': + specifier: ^3.51.0 + version: 3.51.0(@tanstack/react-query@5.56.2(react@18.3.1))(@ts-rest/core@3.51.0(@types/node@20.16.5)(zod@3.23.8))(react@18.3.1)(zod@3.23.8) next: specifier: 14.2.8 version: 14.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -210,6 +210,12 @@ importers: shared: dependencies: + '@nestjs/mapped-types': + specifier: ^2.0.5 + version: 2.0.5(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(reflect-metadata@0.2.2) + '@ts-rest/core': + specifier: ^3.51.0 + version: 3.51.0(@types/node@20.16.5)(zod@3.23.8) class-transformer: specifier: 'catalog:' version: 0.5.1 @@ -759,6 +765,19 @@ packages: 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: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/passport@10.0.3': resolution: {integrity: sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==} peerDependencies: @@ -1079,6 +1098,48 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@tanstack/query-core@5.56.2': + resolution: {integrity: sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==} + + '@tanstack/react-query@5.56.2': + resolution: {integrity: sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==} + peerDependencies: + react: ^18 || ^19 + + '@ts-rest/core@3.51.0': + resolution: {integrity: sha512-v6lnWEcpZj1UgN9wb84XQ+EORP1QEtncFumoXMJjno5ZUV6vdjKze3MYcQN0C6vjBpIJPQEaI/gab2jr4/0KzQ==} + peerDependencies: + '@types/node': ^18.18.7 || >=20.8.4 + zod: ^3.22.3 + peerDependenciesMeta: + '@types/node': + optional: true + zod: + optional: true + + '@ts-rest/nest@3.51.0': + resolution: {integrity: sha512-TYroMQZveatVoOWGNiSgUYt8T+4oWRxwAdt17FZGl6KcY3/83/B1jgwg72ZxMrQigmKBYvhQXlLcw1ASoQrCbw==} + peerDependencies: + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + '@ts-rest/core': ~3.51.0 + rxjs: ^7.1.0 + zod: ^3.22.3 + peerDependenciesMeta: + zod: + optional: true + + '@ts-rest/react-query@3.51.0': + resolution: {integrity: sha512-pWrbyRqvcvmjvm+ORu3zE3sPFqsS6CHOq5vra/UtyLEgXrcnEA+fu/7d9tj/+BLRwe0kOWvalu2S3/d3SDxvFQ==} + peerDependencies: + '@tanstack/react-query': ^4.0.0 || ^5.0.0 + '@ts-rest/core': ~3.51.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + zod: ^3.22.3 + peerDependenciesMeta: + zod: + optional: true + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -5400,6 +5461,13 @@ snapshots: '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.0.5(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + '@nestjs/passport@10.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5812,6 +5880,35 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.7.0 + '@tanstack/query-core@5.56.2': {} + + '@tanstack/react-query@5.56.2(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.56.2 + react: 18.3.1 + + '@ts-rest/core@3.51.0(@types/node@20.16.5)(zod@3.23.8)': + optionalDependencies: + '@types/node': 20.16.5 + zod: 3.23.8 + + '@ts-rest/nest@3.51.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@ts-rest/core@3.51.0(@types/node@20.16.5)(zod@3.23.8))(rxjs@7.8.1)(zod@3.23.8)': + dependencies: + '@nestjs/common': 10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@ts-rest/core': 3.51.0(@types/node@20.16.5)(zod@3.23.8) + rxjs: 7.8.1 + optionalDependencies: + zod: 3.23.8 + + '@ts-rest/react-query@3.51.0(@tanstack/react-query@5.56.2(react@18.3.1))(@ts-rest/core@3.51.0(@types/node@20.16.5)(zod@3.23.8))(react@18.3.1)(zod@3.23.8)': + dependencies: + '@tanstack/react-query': 5.56.2(react@18.3.1) + '@ts-rest/core': 3.51.0(@types/node@20.16.5)(zod@3.23.8) + react: 18.3.1 + optionalDependencies: + zod: 3.23.8 + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -6011,7 +6108,7 @@ snapshots: '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/visitor-keys': 7.2.0 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -6042,7 +6139,7 @@ snapshots: '@typescript-eslint/types': 7.2.0 '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.5.4) '@typescript-eslint/visitor-keys': 7.2.0 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) eslint: 8.57.0 optionalDependencies: typescript: 5.5.4 @@ -6075,7 +6172,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.5.4) '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.5.4) - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: @@ -6106,7 +6203,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.2.0 '@typescript-eslint/visitor-keys': 7.2.0 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -6804,6 +6901,12 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.3.6(supports-color@8.1.1): + dependencies: + ms: 2.1.2 + optionalDependencies: + supports-color: 8.1.1 + dedent@1.5.3: {} deep-equal@2.2.3: @@ -7043,8 +7146,8 @@ snapshots: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.0) eslint-plugin-react: 7.35.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -7067,37 +7170,37 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.0): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7108,7 +7211,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 diff --git a/shared/contracts/auth/auth.contract.ts b/shared/contracts/auth/auth.contract.ts new file mode 100644 index 00000000..bf1ecc66 --- /dev/null +++ b/shared/contracts/auth/auth.contract.ts @@ -0,0 +1,19 @@ +import { initContract } from "@ts-rest/core"; +import { LogInSchema } from "@shared/schemas/auth/login.schema"; +import { UserWithAccessToken } from "@shared/dtos/user.dto"; +import { JSONAPIError } from "@shared/dtos/json-api.error"; + +// TODO: This is a scaffold. We need to define types for responses, zod schemas for body and query param validation etc. + +const contract = initContract(); +export const authContract = contract.router({ + login: { + method: "POST", + path: "/authentication/login", + responses: { + 201: contract.type(), + 401: contract.type(), + }, + body: LogInSchema, + }, +}); diff --git a/shared/contracts/index.ts b/shared/contracts/index.ts new file mode 100644 index 00000000..3f13f97d --- /dev/null +++ b/shared/contracts/index.ts @@ -0,0 +1,8 @@ +import { initContract } from "@ts-rest/core"; +import { authContract } from "./auth/auth.contract"; + +const contract = initContract(); + +export const router = contract.router({ + auth: authContract, +}); diff --git a/shared/dtos/json-api.error.ts b/shared/dtos/json-api.error.ts new file mode 100644 index 00000000..aa5fa802 --- /dev/null +++ b/shared/dtos/json-api.error.ts @@ -0,0 +1,23 @@ +export interface JSONAPIError { + errors: JSONAPIErrorOptions[]; +} + +export interface JSONAPIErrorOptions { + id?: string | undefined; + status?: string | undefined; + code?: string | undefined; + title: string; + detail?: string | undefined; + source?: + | { + pointer?: string | undefined; + parameter?: string | undefined; + } + | undefined; + links?: + | { + about?: string | undefined; + } + | undefined; + meta?: any; +} diff --git a/shared/dtos/user.dto.ts b/shared/dtos/user.dto.ts new file mode 100644 index 00000000..c92b0fc1 --- /dev/null +++ b/shared/dtos/user.dto.ts @@ -0,0 +1,9 @@ +import { OmitType } from "@nestjs/mapped-types"; +import { User } from "@shared/entities/users/user.entity"; + +export type UserWithAccessToken = { + user: UserDto; + accessToken: string; +}; + +export class UserDto extends OmitType(User, ["password"]) {} diff --git a/shared/package.json b/shared/package.json index be9a6248..06be4e40 100644 --- a/shared/package.json +++ b/shared/package.json @@ -7,6 +7,8 @@ "zod": "catalog:" }, "dependencies": { + "@nestjs/mapped-types": "^2.0.5", + "@ts-rest/core": "^3.51.0", "class-transformer": "catalog:" } } diff --git a/shared/schemas/auth/login.schema.ts b/shared/schemas/auth/login.schema.ts new file mode 100644 index 00000000..5f8d743c --- /dev/null +++ b/shared/schemas/auth/login.schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; +export const EmailSchema = z.object({ + email: z + .string({ message: "Email is required" }) + .min(1, "Email is required") + .email("Invalid email"), +}); + +export const PasswordSchema = z.object({ + password: z + .string({ message: "Password is required" }) + .min(1, "Password is required") + .min(8, "Password must be more than 8 characters") + .max(32, "Password must be less than 32 characters"), +}); + +export const LogInSchema = z.intersection(EmailSchema, PasswordSchema);