From ba97186ee95a658e33eb5de06b9965656d7f077d Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 12 Sep 2024 11:51:48 -0700 Subject: [PATCH 01/18] [TM-1271] Follow NestJS patterns to set up the login endpoint. Not yet functional. --- README.md | 1 + apps/user-service/src/app.module.ts | 10 +++++++++ .../src/app/app.controller.spec.ts | 22 ------------------- apps/user-service/src/app/app.controller.ts | 13 ----------- apps/user-service/src/app/app.module.ts | 11 ---------- apps/user-service/src/app/app.service.spec.ts | 21 ------------------ apps/user-service/src/app/app.service.ts | 8 ------- .../src/auth/auth.controller.spec.ts | 18 +++++++++++++++ apps/user-service/src/auth/auth.controller.ts | 16 ++++++++++++++ .../src/auth/auth.service.spec.ts | 18 +++++++++++++++ apps/user-service/src/auth/auth.service.ts | 8 +++++++ apps/user-service/src/auth/dto/login.dto.ts | 4 ++++ .../auth/interfaces/api-response.interface.ts | 3 +++ .../src/auth/interfaces/login.interface.ts | 3 +++ apps/user-service/src/main.ts | 2 +- 15 files changed, 82 insertions(+), 76 deletions(-) create mode 100644 apps/user-service/src/app.module.ts delete mode 100644 apps/user-service/src/app/app.controller.spec.ts delete mode 100644 apps/user-service/src/app/app.controller.ts delete mode 100644 apps/user-service/src/app/app.module.ts delete mode 100644 apps/user-service/src/app/app.service.spec.ts delete mode 100644 apps/user-service/src/app/app.service.ts create mode 100644 apps/user-service/src/auth/auth.controller.spec.ts create mode 100644 apps/user-service/src/auth/auth.controller.ts create mode 100644 apps/user-service/src/auth/auth.service.spec.ts create mode 100644 apps/user-service/src/auth/auth.service.ts create mode 100644 apps/user-service/src/auth/dto/login.dto.ts create mode 100644 apps/user-service/src/auth/interfaces/api-response.interface.ts create mode 100644 apps/user-service/src/auth/interfaces/login.interface.ts diff --git a/README.md b/README.md index 3373dac..d30cef9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Repository for the Microservices API backend of the TerraMatch service * [CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (install globally) * [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) * [NX](https://nx.dev/getting-started/installation#installing-nx-globally) (install globally) + * [NestJS](https://docs.nestjs.com/) (install globally, useful for development) # Building and starting the apps * Copy `.env.local.sample` to `.env` diff --git a/apps/user-service/src/app.module.ts b/apps/user-service/src/app.module.ts new file mode 100644 index 0000000..c7e862c --- /dev/null +++ b/apps/user-service/src/app.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AuthController } from './auth/auth.controller'; +import { AuthService } from './auth/auth.service'; + +@Module({ + imports: [], + controllers: [AuthController], + providers: [AuthService], +}) +export class AppModule {} diff --git a/apps/user-service/src/app/app.controller.spec.ts b/apps/user-service/src/app/app.controller.spec.ts deleted file mode 100644 index de8007e..0000000 --- a/apps/user-service/src/app/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let app: TestingModule; - - beforeAll(async () => { - app = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - }); - - describe('getData', () => { - it('should return "Hello API"', () => { - const appController = app.get(AppController); - expect(appController.getData()).toEqual({ message: 'Hello API' }); - }); - }); -}); diff --git a/apps/user-service/src/app/app.controller.ts b/apps/user-service/src/app/app.controller.ts deleted file mode 100644 index 5fa28cf..0000000 --- a/apps/user-service/src/app/app.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(protected readonly appService: AppService) {} - - @Get('auth/login') - login() { - return this.appService.login(); - } -} diff --git a/apps/user-service/src/app/app.module.ts b/apps/user-service/src/app/app.module.ts deleted file mode 100644 index 6a9bc16..0000000 --- a/apps/user-service/src/app/app.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -@Module({ - imports: [], - controllers: [AppController], - providers: [AppService], -}) -export class AppModule {} diff --git a/apps/user-service/src/app/app.service.spec.ts b/apps/user-service/src/app/app.service.spec.ts deleted file mode 100644 index 42cf0a2..0000000 --- a/apps/user-service/src/app/app.service.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Test } from '@nestjs/testing'; - -import { AppService } from './app.service'; - -describe('AppService', () => { - let service: AppService; - - beforeAll(async () => { - const app = await Test.createTestingModule({ - providers: [AppService], - }).compile(); - - service = app.get(AppService); - }); - - describe('getData', () => { - it('should return "Hello API"', () => { - expect(service.getData()).toEqual({ message: 'Hello API' }); - }); - }); -}); diff --git a/apps/user-service/src/app/app.service.ts b/apps/user-service/src/app/app.service.ts deleted file mode 100644 index 5cba6e1..0000000 --- a/apps/user-service/src/app/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - login(): { message: string } { - return { message: 'Login endpoint' }; - } -} diff --git a/apps/user-service/src/auth/auth.controller.spec.ts b/apps/user-service/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..27a31e6 --- /dev/null +++ b/apps/user-service/src/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/user-service/src/auth/auth.controller.ts b/apps/user-service/src/auth/auth.controller.ts new file mode 100644 index 0000000..9f395a7 --- /dev/null +++ b/apps/user-service/src/auth/auth.controller.ts @@ -0,0 +1,16 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { LoginDto } from './dto/login.dto'; +import { ApiResponse } from './interfaces/api-response.interface'; +import { Login } from './interfaces/login.interface'; + +@Controller('auth') +export class AuthController { + constructor (protected readonly authService: AuthService) {} + + @Post('login') + login(@Body() loginDto: LoginDto): ApiResponse { + const token = this.authService.login(loginDto.email_address, loginDto.password); + return { data: { token } }; + } +} diff --git a/apps/user-service/src/auth/auth.service.spec.ts b/apps/user-service/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/apps/user-service/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/user-service/src/auth/auth.service.ts b/apps/user-service/src/auth/auth.service.ts new file mode 100644 index 0000000..bc546cd --- /dev/null +++ b/apps/user-service/src/auth/auth.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AuthService { + login(email_address: string, password: string) { + return `Auth Service [${email_address}, ${password}]`; + } +} diff --git a/apps/user-service/src/auth/dto/login.dto.ts b/apps/user-service/src/auth/dto/login.dto.ts new file mode 100644 index 0000000..9d92b6c --- /dev/null +++ b/apps/user-service/src/auth/dto/login.dto.ts @@ -0,0 +1,4 @@ +export class LoginDto { + email_address: string; + password: string; +} diff --git a/apps/user-service/src/auth/interfaces/api-response.interface.ts b/apps/user-service/src/auth/interfaces/api-response.interface.ts new file mode 100644 index 0000000..3f99b57 --- /dev/null +++ b/apps/user-service/src/auth/interfaces/api-response.interface.ts @@ -0,0 +1,3 @@ +export interface ApiResponse { + data: DataT; +} diff --git a/apps/user-service/src/auth/interfaces/login.interface.ts b/apps/user-service/src/auth/interfaces/login.interface.ts new file mode 100644 index 0000000..21efebe --- /dev/null +++ b/apps/user-service/src/auth/interfaces/login.interface.ts @@ -0,0 +1,3 @@ +export interface Login { + token: string; +} diff --git a/apps/user-service/src/main.ts b/apps/user-service/src/main.ts index 7f44b00..922fc7e 100644 --- a/apps/user-service/src/main.ts +++ b/apps/user-service/src/main.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app/app.module'; +import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); From b915f91c86abb455963414960dc78fe0721b2104 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 12 Sep 2024 14:41:49 -0700 Subject: [PATCH 02/18] [TM-1271] Adds swagger docs, validation and enforcement of the JSON API response structure. --- apps/user-service/src/auth/auth.controller.ts | 13 +- apps/user-service/src/auth/dto/login.dto.ts | 8 + .../src/auth/entities/login.entity.ts | 6 + .../auth/interfaces/api-response.interface.ts | 3 - .../src/auth/interfaces/login.interface.ts | 3 - .../decorators/json-api-response.decorator.ts | 29 +++ .../src/interceptors/transform.interceptor.ts | 30 +++ apps/user-service/src/main.ts | 17 +- package-lock.json | 205 ++++++++++++++---- package.json | 3 + 10 files changed, 260 insertions(+), 57 deletions(-) create mode 100644 apps/user-service/src/auth/entities/login.entity.ts delete mode 100644 apps/user-service/src/auth/interfaces/api-response.interface.ts delete mode 100644 apps/user-service/src/auth/interfaces/login.interface.ts create mode 100644 apps/user-service/src/decorators/json-api-response.decorator.ts create mode 100644 apps/user-service/src/interceptors/transform.interceptor.ts diff --git a/apps/user-service/src/auth/auth.controller.ts b/apps/user-service/src/auth/auth.controller.ts index 9f395a7..0459c2b 100644 --- a/apps/user-service/src/auth/auth.controller.ts +++ b/apps/user-service/src/auth/auth.controller.ts @@ -1,16 +1,21 @@ import { Body, Controller, Post } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LoginDto } from './dto/login.dto'; -import { ApiResponse } from './interfaces/api-response.interface'; -import { Login } from './interfaces/login.interface'; +import { Login } from './entities/login.entity'; +import { JsonApiResponse } from '../decorators/json-api-response.decorator'; @Controller('auth') export class AuthController { constructor (protected readonly authService: AuthService) {} @Post('login') - login(@Body() loginDto: LoginDto): ApiResponse { + @JsonApiResponse({ + status: 201, + description: 'Receive a JWT Token in exchange for login credentials', + dataType: Login + }) + login(@Body() loginDto: LoginDto): Login { const token = this.authService.login(loginDto.email_address, loginDto.password); - return { data: { token } }; + return { token }; } } diff --git a/apps/user-service/src/auth/dto/login.dto.ts b/apps/user-service/src/auth/dto/login.dto.ts index 9d92b6c..2043aff 100644 --- a/apps/user-service/src/auth/dto/login.dto.ts +++ b/apps/user-service/src/auth/dto/login.dto.ts @@ -1,4 +1,12 @@ +import { IsEmail, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + export class LoginDto { + @IsEmail() + @ApiProperty() email_address: string; + + @IsNotEmpty() + @ApiProperty() password: string; } diff --git a/apps/user-service/src/auth/entities/login.entity.ts b/apps/user-service/src/auth/entities/login.entity.ts new file mode 100644 index 0000000..7b93f86 --- /dev/null +++ b/apps/user-service/src/auth/entities/login.entity.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class Login { + @ApiProperty({ description: 'JWT token for use in future authenticated requests to the API.' }) + token: string; +} diff --git a/apps/user-service/src/auth/interfaces/api-response.interface.ts b/apps/user-service/src/auth/interfaces/api-response.interface.ts deleted file mode 100644 index 3f99b57..0000000 --- a/apps/user-service/src/auth/interfaces/api-response.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ApiResponse { - data: DataT; -} diff --git a/apps/user-service/src/auth/interfaces/login.interface.ts b/apps/user-service/src/auth/interfaces/login.interface.ts deleted file mode 100644 index 21efebe..0000000 --- a/apps/user-service/src/auth/interfaces/login.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Login { - token: string; -} diff --git a/apps/user-service/src/decorators/json-api-response.decorator.ts b/apps/user-service/src/decorators/json-api-response.decorator.ts new file mode 100644 index 0000000..5b2bfb1 --- /dev/null +++ b/apps/user-service/src/decorators/json-api-response.decorator.ts @@ -0,0 +1,29 @@ +import { ApiExtraModels, ApiResponse, ApiResponseOptions, getSchemaPath } from '@nestjs/swagger'; + +/** + * Decorator to simplify wrapping the response type from a controller method with the JSON API + * response structure. Applies the ApiExtraModels and ApiResponse decorators. + */ +export function JsonApiResponse(options: ApiResponseOptions & { dataType: Function }): MethodDecorator & ClassDecorator { + const { dataType, ...rest } = options; + const apiResponseOptions = { + ...rest, + schema: { + type: "object", + properties: { + data: { + type: "object", + $ref: getSchemaPath(dataType) + } + } + } + } as ApiResponseOptions + + return ( + target: object, + key?: string | symbol, + descriptor?: TypedPropertyDescriptor + ): any => { + return ApiResponse(apiResponseOptions)(ApiExtraModels(dataType)(target, key, descriptor), key, descriptor); + } +} diff --git a/apps/user-service/src/interceptors/transform.interceptor.ts b/apps/user-service/src/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..c1621d7 --- /dev/null +++ b/apps/user-service/src/interceptors/transform.interceptor.ts @@ -0,0 +1,30 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export interface Response { + data: T; +} + +/** + * Transforms all method responses into a JSON API response structure. + * TODO: support additional properties (beyond just data) on the response structure. This will + * likely involve returning a custom response object in those cases that this interceptor knows + * to look for. + */ +@Injectable() +export class TransformInterceptor + implements NestInterceptor> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { + return next.handle().pipe(map(data => ({ data }))); + } +} diff --git a/apps/user-service/src/main.ts b/apps/user-service/src/main.ts index 922fc7e..7a5279f 100644 --- a/apps/user-service/src/main.ts +++ b/apps/user-service/src/main.ts @@ -1,10 +1,25 @@ -import { Logger } from '@nestjs/common'; +import { Logger, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { TransformInterceptor } from './interceptors/transform.interceptor'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + const config = new DocumentBuilder() + .setTitle('TerraMatch User Service') + .setDescription('APIs related to login, users and organisations.') + .setVersion('1.0') + .addTag('user-service') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + + app.useGlobalPipes(new ValidationPipe()); + app.useGlobalInterceptors(new TransformInterceptor()); + const port = process.env.PORT ?? 4010; await app.listen(port); Logger.log( diff --git a/package-lock.json b/package-lock.json index be57e4a..2a4086e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,10 @@ "@nestjs/common": "^10.0.2", "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", + "@nestjs/swagger": "^7.4.0", "axios": "^1.6.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", "tslib": "^2.3.0" @@ -2737,6 +2740,11 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==" + }, "node_modules/@module-federation/bridge-react-webpack-plugin": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.6.3.tgz", @@ -3066,6 +3074,25 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "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 + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", @@ -3113,6 +3140,54 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true }, + "node_modules/@nestjs/swagger": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz", + "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==", + "dependencies": { + "@microsoft/tsdoc": "^0.15.0", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.17.14" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/swagger/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@nestjs/swagger/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@nestjs/testing": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", @@ -3445,6 +3520,33 @@ } } }, + "node_modules/@nx/js/node_modules/babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "node_modules/@nx/js/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@nx/js/node_modules/ora": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", @@ -4454,6 +4556,11 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.1.tgz", + "integrity": "sha512-w0URwf7BQb0rD/EuiG12KP0bailHKHP5YVviJG9zw3ykAokL0TuxU2TUqMB7EwZ59bDHYdeTIvjI5m0S7qHfOA==" + }, "node_modules/@types/ws": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", @@ -5328,14 +5435,20 @@ } }, "node_modules/babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "dev": true, + "optional": true, + "peer": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" } }, "node_modules/babel-plugin-polyfill-corejs2": { @@ -5879,6 +5992,21 @@ "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6361,19 +6489,19 @@ } }, "node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, "dependencies": { "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", + "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "^1.7.2" + "yaml": "^1.10.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/create-jest": { @@ -8008,22 +8136,6 @@ "concat-map": "0.0.1" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -10288,6 +10400,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.8.tgz", + "integrity": "sha512-0fv/YKpJBAgXKy0kaS3fnqoUVN8901vUYAKIGD/MWZaDfhJt1nZjPL3ZzdZBt/G8G8Hw2J1xOIrXWdNHFHPAvg==" + }, "node_modules/license-webpack-plugin": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", @@ -10364,8 +10481,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.clonedeepwith": { "version": "4.5.0", @@ -11643,22 +11759,6 @@ "webpack": "^5.0.0" } }, - "node_modules/postcss-loader/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/postcss-merge-longhand": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", @@ -13387,6 +13487,11 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -14127,6 +14232,14 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index c9651e8..edc9e3f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,10 @@ "@nestjs/common": "^10.0.2", "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", + "@nestjs/swagger": "^7.4.0", "axios": "^1.6.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", "tslib": "^2.3.0" From 718f33ccf4260f6e338361e13703ddd61f30f8ea Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 12 Sep 2024 14:49:40 -0700 Subject: [PATCH 03/18] [TM-1271] Remove unused test from api-gateway. --- apps/api-gateway/test/api-gateway.test.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 apps/api-gateway/test/api-gateway.test.ts diff --git a/apps/api-gateway/test/api-gateway.test.ts b/apps/api-gateway/test/api-gateway.test.ts deleted file mode 100644 index 9ead2f9..0000000 --- a/apps/api-gateway/test/api-gateway.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -// import * as cdk from 'aws-cdk-lib'; -// import { Template } from 'aws-cdk-lib/assertions'; -// import * as ApiGateway from '../lib/api-gateway-stack'; - -// example test. To run these tests, uncomment this file along with the -// example resource in lib/api-gateway-stack.ts -test('SQS Queue Created', () => { -// const app = new cdk.App(); -// // WHEN -// const stack = new ApiGateway.ApiGatewayStack(app, 'MyTestStack'); -// // THEN -// const template = Template.fromStack(stack); - -// template.hasResourceProperties('AWS::SQS::Queue', { -// VisibilityTimeout: 300 -// }); -}); From ae4ebc8ea531488dc7fb84e11141c43a0f012efc Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 12 Sep 2024 16:58:59 -0700 Subject: [PATCH 04/18] [TM-1271] Initial DB connection and implementation of the User model. --- apps/api-gateway/jest.config.js | 8 - apps/user-service/src/app.module.ts | 7 +- apps/user-service/src/auth/auth.controller.ts | 10 +- apps/user-service/src/auth/auth.service.ts | 10 +- .../{login.dto.ts => login-request.dto.ts} | 2 +- .../login-response.dto.ts} | 2 +- libs/database/.eslintrc.json | 18 + libs/database/README.md | 7 + libs/database/jest.config.ts | 11 + libs/database/project.json | 9 + libs/database/src/index.ts | 2 + libs/database/src/lib/database.module.ts | 19 + libs/database/src/lib/entities/user.entity.ts | 92 ++ .../src/lib/typeorm-config.service.ts | 21 + libs/database/tsconfig.json | 22 + libs/database/tsconfig.lib.json | 17 + libs/database/tsconfig.spec.json | 14 + package-lock.json | 790 ++++++++++++++++-- package.json | 6 +- tsconfig.base.json | 4 +- 20 files changed, 1003 insertions(+), 68 deletions(-) delete mode 100644 apps/api-gateway/jest.config.js rename apps/user-service/src/auth/dto/{login.dto.ts => login-request.dto.ts} (88%) rename apps/user-service/src/auth/{entities/login.entity.ts => dto/login-response.dto.ts} (85%) create mode 100644 libs/database/.eslintrc.json create mode 100644 libs/database/README.md create mode 100644 libs/database/jest.config.ts create mode 100644 libs/database/project.json create mode 100644 libs/database/src/index.ts create mode 100644 libs/database/src/lib/database.module.ts create mode 100644 libs/database/src/lib/entities/user.entity.ts create mode 100644 libs/database/src/lib/typeorm-config.service.ts create mode 100644 libs/database/tsconfig.json create mode 100644 libs/database/tsconfig.lib.json create mode 100644 libs/database/tsconfig.spec.json diff --git a/apps/api-gateway/jest.config.js b/apps/api-gateway/jest.config.js deleted file mode 100644 index 08263b8..0000000 --- a/apps/api-gateway/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - testEnvironment: 'node', - roots: ['/test'], - testMatch: ['**/*.test.ts'], - transform: { - '^.+\\.tsx?$': 'ts-jest' - } -}; diff --git a/apps/user-service/src/app.module.ts b/apps/user-service/src/app.module.ts index c7e862c..5a91813 100644 --- a/apps/user-service/src/app.module.ts +++ b/apps/user-service/src/app.module.ts @@ -1,9 +1,14 @@ import { Module } from '@nestjs/common'; import { AuthController } from './auth/auth.controller'; import { AuthService } from './auth/auth.service'; +import { DatabaseModule, User } from '@terramatch-microservices/database'; +import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ - imports: [], + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([User]) + ], controllers: [AuthController], providers: [AuthService], }) diff --git a/apps/user-service/src/auth/auth.controller.ts b/apps/user-service/src/auth/auth.controller.ts index 0459c2b..d719a0a 100644 --- a/apps/user-service/src/auth/auth.controller.ts +++ b/apps/user-service/src/auth/auth.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Post } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { LoginDto } from './dto/login.dto'; -import { Login } from './entities/login.entity'; +import { LoginRequest } from './dto/login-request.dto'; import { JsonApiResponse } from '../decorators/json-api-response.decorator'; +import { LoginResponse } from './dto/login-response.dto'; @Controller('auth') export class AuthController { @@ -12,10 +12,10 @@ export class AuthController { @JsonApiResponse({ status: 201, description: 'Receive a JWT Token in exchange for login credentials', - dataType: Login + dataType: LoginResponse }) - login(@Body() loginDto: LoginDto): Login { - const token = this.authService.login(loginDto.email_address, loginDto.password); + async login(@Body() { email_address, password }: LoginRequest): Promise { + const token = await this.authService.login(email_address, password); return { token }; } } diff --git a/apps/user-service/src/auth/auth.service.ts b/apps/user-service/src/auth/auth.service.ts index bc546cd..46607fd 100644 --- a/apps/user-service/src/auth/auth.service.ts +++ b/apps/user-service/src/auth/auth.service.ts @@ -1,8 +1,14 @@ import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { User } from '@terramatch-microservices/database'; @Injectable() export class AuthService { - login(email_address: string, password: string) { - return `Auth Service [${email_address}, ${password}]`; + constructor(private readonly dataSource: DataSource) {} + + async login(email_address: string, password: string) { + const user = await this.dataSource.manager.findOneBy(User, { email_address }); + + return `Auth Service [${email_address}, ${user.id}, ${user.uuid}]`; } } diff --git a/apps/user-service/src/auth/dto/login.dto.ts b/apps/user-service/src/auth/dto/login-request.dto.ts similarity index 88% rename from apps/user-service/src/auth/dto/login.dto.ts rename to apps/user-service/src/auth/dto/login-request.dto.ts index 2043aff..93badc4 100644 --- a/apps/user-service/src/auth/dto/login.dto.ts +++ b/apps/user-service/src/auth/dto/login-request.dto.ts @@ -1,7 +1,7 @@ import { IsEmail, IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -export class LoginDto { +export class LoginRequest { @IsEmail() @ApiProperty() email_address: string; diff --git a/apps/user-service/src/auth/entities/login.entity.ts b/apps/user-service/src/auth/dto/login-response.dto.ts similarity index 85% rename from apps/user-service/src/auth/entities/login.entity.ts rename to apps/user-service/src/auth/dto/login-response.dto.ts index 7b93f86..ed33aaa 100644 --- a/apps/user-service/src/auth/entities/login.entity.ts +++ b/apps/user-service/src/auth/dto/login-response.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class Login { +export class LoginResponse { @ApiProperty({ description: 'JWT token for use in future authenticated requests to the API.' }) token: string; } diff --git a/libs/database/.eslintrc.json b/libs/database/.eslintrc.json new file mode 100644 index 0000000..9d9c0db --- /dev/null +++ b/libs/database/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/database/README.md b/libs/database/README.md new file mode 100644 index 0000000..8453478 --- /dev/null +++ b/libs/database/README.md @@ -0,0 +1,7 @@ +# database + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test database` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/database/jest.config.ts b/libs/database/jest.config.ts new file mode 100644 index 0000000..6fef25b --- /dev/null +++ b/libs/database/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'database', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/libs/database', +}; diff --git a/libs/database/project.json b/libs/database/project.json new file mode 100644 index 0000000..b87410e --- /dev/null +++ b/libs/database/project.json @@ -0,0 +1,9 @@ +{ + "name": "database", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/database/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project database --web", + "targets": {} +} diff --git a/libs/database/src/index.ts b/libs/database/src/index.ts new file mode 100644 index 0000000..933657d --- /dev/null +++ b/libs/database/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/database.module'; +export * from './lib/entities/user.entity'; diff --git a/libs/database/src/lib/database.module.ts b/libs/database/src/lib/database.module.ts new file mode 100644 index 0000000..d5d4854 --- /dev/null +++ b/libs/database/src/lib/database.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TypeOrmConfigService } from './typeorm-config.service'; +import { User } from './entities/user.entity'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forRootAsync({ + useClass: TypeOrmConfigService, + imports: [ConfigModule], + }) + ], + controllers: [], + providers: [], + exports: [], +}) +export class DatabaseModule {} diff --git a/libs/database/src/lib/entities/user.entity.ts b/libs/database/src/lib/entities/user.entity.ts new file mode 100644 index 0000000..63a68f9 --- /dev/null +++ b/libs/database/src/lib/entities/user.entity.ts @@ -0,0 +1,92 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn +} from 'typeorm'; + +@Entity({ name: 'users' }) +export class User { + @PrimaryGeneratedColumn() + id: number; + + @Column('uuid') + uuid: string; + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; + + @DeleteDateColumn() + deleted_at: Date; + + // TODO: relate this to the organisations table + // Note: this is a bigint(20) unsigned according to describe users, but TypeORM claims that + // unsigned big int isn't supported by MariaDB. + @Column('bigint') + organisation_id: number; + + @Column() + first_name: string; + + @Column() + last_name: string; + + @Column() + email_address: string; + + @Column() + password: string; + + @Column() + email_address_verified_at: Date; + + @Column() + last_logged_in_at: Date; + + @Column() + job_role: string; + + @Column() + facebook: string; + + @Column() + twitter: string; + + @Column() + linkedin: string; + + @Column() + instagram: string; + + @Column() + avatar: string; + + @Column() + phone_number: string; + + @Column() + whatsapp_phone: string; + + @Column('bool') + is_subscribed: boolean; + + @Column('bool') + has_consented: boolean; + + @Column() + banners: string; + + @Column() + api_key: string; + + @Column() + country: string; + + @Column() + program: string; +} diff --git a/libs/database/src/lib/typeorm-config.service.ts b/libs/database/src/lib/typeorm-config.service.ts new file mode 100644 index 0000000..e9121fc --- /dev/null +++ b/libs/database/src/lib/typeorm-config.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; +import { ConfigService } from '@nestjs/config'; +import { User } from './entities/user.entity'; + +@Injectable() +export class TypeOrmConfigService implements TypeOrmOptionsFactory { + constructor(protected readonly configService: ConfigService) {} + + createTypeOrmOptions(): TypeOrmModuleOptions { + return { + type: 'mariadb', + host: this.configService.get('DB_HOST'), + port: this.configService.get('DB_PORT'), + username: this.configService.get('DB_USERNAME'), + password: this.configService.get('DB_PASSWORD'), + database: this.configService.get('DB_DATABASE'), + entities: [User], + }; + } +} diff --git a/libs/database/tsconfig.json b/libs/database/tsconfig.json new file mode 100644 index 0000000..f5b8565 --- /dev/null +++ b/libs/database/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/database/tsconfig.lib.json b/libs/database/tsconfig.lib.json new file mode 100644 index 0000000..8abc0ae --- /dev/null +++ b/libs/database/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "strictPropertyInitialization": false, + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/database/tsconfig.spec.json b/libs/database/tsconfig.spec.json new file mode 100644 index 0000000..9b2a121 --- /dev/null +++ b/libs/database/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index 2a4086e..336a9b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,19 @@ "license": "MIT", "dependencies": { "@nestjs/common": "^10.0.2", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", "@nestjs/swagger": "^7.4.0", + "@nestjs/typeorm": "^10.0.2", "axios": "^1.6.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "mysql2": "^3.11.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "typeorm": "^0.3.20" }, "devDependencies": { "@nestjs/schematics": "^10.0.1", @@ -2054,7 +2058,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -2066,7 +2070,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -2311,6 +2315,95 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2632,7 +2725,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -2660,7 +2753,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -3032,6 +3125,28 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, + "node_modules/@nestjs/config": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", + "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } + }, "node_modules/@nestjs/core": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", @@ -3221,6 +3336,33 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, + "node_modules/@nestjs/typeorm": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", + "integrity": "sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==", + "dependencies": { + "uuid": "9.0.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, + "node_modules/@nestjs/typeorm/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3931,6 +4073,15 @@ "typescript": "^3 || ^4 || ^5" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3955,6 +4106,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" + }, "node_modules/@swc-node/core": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.13.3.tgz", @@ -4258,25 +4414,25 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true + "devOptional": true }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", @@ -4470,7 +4626,7 @@ "version": "18.16.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.20.tgz", "integrity": "sha512-nL54VfDjThdP2UXJXZao5wp76CDiDw4zSRO8d4Tk7UgDqNKGKVEQB0/t3ti63NS+YNNkIQDvwEAF04BO+WYu7Q==", - "dev": true + "devOptional": true }, "node_modules/@types/node-forge": { "version": "1.3.11", @@ -5046,7 +5202,7 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -5076,7 +5232,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "dependencies": { "acorn": "^8.11.0" }, @@ -5187,7 +5343,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5206,6 +5361,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -5219,6 +5379,14 @@ "node": ">= 8" } }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -5228,7 +5396,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/argparse": { "version": "1.0.10", @@ -5316,6 +5484,14 @@ "postcss": "^8.1.0" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axios": { "version": "1.7.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", @@ -5553,14 +5729,12 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5688,7 +5862,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -6019,6 +6192,66 @@ "node": ">=8" } }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, "node_modules/cli-spinners": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", @@ -6035,7 +6268,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -6529,7 +6761,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "node_modules/cron-parser": { "version": "4.9.0", @@ -6547,7 +6779,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6819,11 +7050,15 @@ "node": ">=4.0" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -6950,6 +7185,14 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7003,7 +7246,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -7112,7 +7355,6 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, "engines": { "node": ">=12" }, @@ -7141,6 +7383,11 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7182,8 +7429,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/emojis-list": { "version": "3.0.0", @@ -7311,7 +7557,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "engines": { "node": ">=6" } @@ -8067,6 +8312,32 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "7.2.13", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz", @@ -8291,6 +8562,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8304,7 +8583,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -8616,6 +8894,14 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -8911,7 +9197,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -9120,7 +9405,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -9254,6 +9538,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -9313,8 +9602,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", @@ -9417,6 +9705,20 @@ "node": ">=6" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -10545,6 +10847,11 @@ "node": ">=8.0" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", @@ -10560,6 +10867,20 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.0.tgz", + "integrity": "sha512-86xXMB6DiuKrTqkE/lRL0drlNh568awttBPJ7D66fzDHpy6NC5r3N+Ly/lKCS2zjmeGyvFDx670z0cD0PVBwGA==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/luxon": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", @@ -10600,7 +10921,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -10765,6 +11086,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -10811,6 +11140,65 @@ "multicast-dns": "cli.js" } }, + "node_modules/mysql2": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.2.tgz", + "integrity": "sha512-3jhjAk4NHs3rcKjOiFTqmU76kdib/KDOC+lshrYa76QWkcfF1GbYGK4d5PqPljVmIAc0ChozCRmeYIlNp5bz5w==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -11305,6 +11693,11 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11365,6 +11758,19 @@ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", "dev": true }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -11395,7 +11801,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -11406,6 +11811,26 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/path-to-regexp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", @@ -12485,7 +12910,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12840,6 +13264,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -12962,6 +13391,18 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -12978,7 +13419,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -12990,7 +13430,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -13164,6 +13603,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -13273,7 +13720,20 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -13287,7 +13747,18 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -13714,6 +14185,25 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/thingies": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", @@ -13887,7 +14377,7 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14033,11 +14523,203 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, + "node_modules/typeorm": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", + "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "chalk": "^4.1.2", + "cli-highlight": "^2.1.11", + "dayjs": "^1.11.9", + "debug": "^4.3.4", + "dotenv": "^16.0.3", + "glob": "^10.3.10", + "mkdirp": "^2.1.3", + "reflect-metadata": "^0.2.1", + "sha.js": "^2.4.11", + "tslib": "^2.5.0", + "uuid": "^9.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0", + "@sap/hana-client": "^2.12.25", + "better-sqlite3": "^7.1.2 || ^8.0.0 || ^9.0.0", + "hdb-pool": "^0.1.6", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0", + "mssql": "^9.1.1 || ^10.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14207,7 +14889,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -14781,7 +15463,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -14811,7 +15492,23 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -14876,7 +15573,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -14900,7 +15596,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -14918,7 +15613,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -14936,7 +15630,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } diff --git a/package.json b/package.json index edc9e3f..2ff8244 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,19 @@ "private": true, "dependencies": { "@nestjs/common": "^10.0.2", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", "@nestjs/swagger": "^7.4.0", + "@nestjs/typeorm": "^10.0.2", "axios": "^1.6.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "mysql2": "^3.11.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "typeorm": "^0.3.20" }, "devDependencies": { "@nestjs/schematics": "^10.0.1", diff --git a/tsconfig.base.json b/tsconfig.base.json index b73cce6..7d98e72 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,7 +14,9 @@ "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", - "paths": {} + "paths": { + "@terramatch-microservices/database": ["libs/database/src/index.ts"] + } }, "exclude": ["node_modules", "tmp"] } From 320eb2c75a88397fa6ace5623c4b7e99fe2094c8 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 12 Sep 2024 17:02:10 -0700 Subject: [PATCH 05/18] [TM-1271] Use the ActiveRecord pattern. --- apps/user-service/src/app.module.ts | 8 ++------ apps/user-service/src/auth/auth.service.ts | 5 +---- libs/database/src/lib/entities/user.entity.ts | 3 ++- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/user-service/src/app.module.ts b/apps/user-service/src/app.module.ts index 5a91813..f48e465 100644 --- a/apps/user-service/src/app.module.ts +++ b/apps/user-service/src/app.module.ts @@ -1,14 +1,10 @@ import { Module } from '@nestjs/common'; import { AuthController } from './auth/auth.controller'; import { AuthService } from './auth/auth.service'; -import { DatabaseModule, User } from '@terramatch-microservices/database'; -import { TypeOrmModule } from '@nestjs/typeorm'; +import { DatabaseModule } from '@terramatch-microservices/database'; @Module({ - imports: [ - DatabaseModule, - TypeOrmModule.forFeature([User]) - ], + imports: [DatabaseModule], controllers: [AuthController], providers: [AuthService], }) diff --git a/apps/user-service/src/auth/auth.service.ts b/apps/user-service/src/auth/auth.service.ts index 46607fd..5a59f14 100644 --- a/apps/user-service/src/auth/auth.service.ts +++ b/apps/user-service/src/auth/auth.service.ts @@ -1,13 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; import { User } from '@terramatch-microservices/database'; @Injectable() export class AuthService { - constructor(private readonly dataSource: DataSource) {} - async login(email_address: string, password: string) { - const user = await this.dataSource.manager.findOneBy(User, { email_address }); + const user = await User.findOneBy({ email_address }); return `Auth Service [${email_address}, ${user.id}, ${user.uuid}]`; } diff --git a/libs/database/src/lib/entities/user.entity.ts b/libs/database/src/lib/entities/user.entity.ts index 63a68f9..14783e9 100644 --- a/libs/database/src/lib/entities/user.entity.ts +++ b/libs/database/src/lib/entities/user.entity.ts @@ -1,4 +1,5 @@ import { + BaseEntity, Column, CreateDateColumn, DeleteDateColumn, @@ -8,7 +9,7 @@ import { } from 'typeorm'; @Entity({ name: 'users' }) -export class User { +export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; From 0aeb9ee1c45bceac86227c8e69c3bfce5c7930f5 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 13 Sep 2024 10:21:04 -0700 Subject: [PATCH 06/18] [TM-1271] Use camel case in all code and API responses. --- apps/user-service/src/auth/auth.controller.ts | 4 +- apps/user-service/src/auth/auth.service.ts | 9 ++- .../src/auth/dto/login-request.dto.ts | 2 +- libs/database/src/lib/entities/user.entity.ts | 60 +++++++++---------- package-lock.json | 6 ++ package.json | 1 + 6 files changed, 46 insertions(+), 36 deletions(-) diff --git a/apps/user-service/src/auth/auth.controller.ts b/apps/user-service/src/auth/auth.controller.ts index d719a0a..d629bfb 100644 --- a/apps/user-service/src/auth/auth.controller.ts +++ b/apps/user-service/src/auth/auth.controller.ts @@ -14,8 +14,8 @@ export class AuthController { description: 'Receive a JWT Token in exchange for login credentials', dataType: LoginResponse }) - async login(@Body() { email_address, password }: LoginRequest): Promise { - const token = await this.authService.login(email_address, password); + async login(@Body() { emailAddress, password }: LoginRequest): Promise { + const token = await this.authService.login(emailAddress, password); return { token }; } } diff --git a/apps/user-service/src/auth/auth.service.ts b/apps/user-service/src/auth/auth.service.ts index 5a59f14..85dee9e 100644 --- a/apps/user-service/src/auth/auth.service.ts +++ b/apps/user-service/src/auth/auth.service.ts @@ -3,9 +3,12 @@ import { User } from '@terramatch-microservices/database'; @Injectable() export class AuthService { - async login(email_address: string, password: string) { - const user = await User.findOneBy({ email_address }); + async login(emailAddress: string, password: string) { + // TODO: what additional fields do we need for JWT generation? This could simply be + // User.findOneBy(), but it's nice not to have to pull the whole role from this fairly large + // table + const { password: dbPassword } = await User.findOne({ select: { password: true }, where: { emailAddress } }); - return `Auth Service [${email_address}, ${user.id}, ${user.uuid}]`; + return `Auth Service [${dbPassword}]`; } } diff --git a/apps/user-service/src/auth/dto/login-request.dto.ts b/apps/user-service/src/auth/dto/login-request.dto.ts index 93badc4..00ca2ca 100644 --- a/apps/user-service/src/auth/dto/login-request.dto.ts +++ b/apps/user-service/src/auth/dto/login-request.dto.ts @@ -4,7 +4,7 @@ import { ApiProperty } from '@nestjs/swagger'; export class LoginRequest { @IsEmail() @ApiProperty() - email_address: string; + emailAddress: string; @IsNotEmpty() @ApiProperty() diff --git a/libs/database/src/lib/entities/user.entity.ts b/libs/database/src/lib/entities/user.entity.ts index 14783e9..48ceee7 100644 --- a/libs/database/src/lib/entities/user.entity.ts +++ b/libs/database/src/lib/entities/user.entity.ts @@ -16,41 +16,41 @@ export class User extends BaseEntity { @Column('uuid') uuid: string; - @CreateDateColumn() - created_at: Date; + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; - @UpdateDateColumn() - updated_at: Date; + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; - @DeleteDateColumn() - deleted_at: Date; + @DeleteDateColumn({ name: 'deleted_at' }) + deletedAt: Date; // TODO: relate this to the organisations table // Note: this is a bigint(20) unsigned according to describe users, but TypeORM claims that // unsigned big int isn't supported by MariaDB. - @Column('bigint') - organisation_id: number; + @Column('bigint', { name: 'organisation_id' }) + organisationId: number; - @Column() - first_name: string; + @Column({ name: 'first_name' }) + firstName: string; - @Column() - last_name: string; + @Column({ name: 'last_name' }) + lastName: string; - @Column() - email_address: string; + @Column({ name: 'email_address' }) + emailAddress: string; @Column() password: string; - @Column() - email_address_verified_at: Date; + @Column({ name: 'email_address_verified_at' }) + emailAddressVerifiedAt: Date; - @Column() - last_logged_in_at: Date; + @Column({ name: 'last_logged_in_at' }) + lastLoggedInAt: Date; - @Column() - job_role: string; + @Column({ name: 'job_role' }) + jobRole: string; @Column() facebook: string; @@ -67,23 +67,23 @@ export class User extends BaseEntity { @Column() avatar: string; - @Column() - phone_number: string; + @Column({ name: 'phone_number' }) + phoneNumber: string; - @Column() - whatsapp_phone: string; + @Column({ name: 'whatsapp_phone' }) + whatsappPhone: string; - @Column('bool') - is_subscribed: boolean; + @Column('bool', { name: 'is_subscribed' }) + isSubscribed: boolean; - @Column('bool') - has_consented: boolean; + @Column('bool', { name: 'has_consented' }) + hasConsented: boolean; @Column() banners: string; - @Column() - api_key: string; + @Column({ name: 'api_key' }) + apiKey: string; @Column() country: string; diff --git a/package-lock.json b/package-lock.json index 336a9b3..d1019c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", "axios": "^1.6.0", + "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "mysql2": "^3.11.2", @@ -5774,6 +5775,11 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", diff --git a/package.json b/package.json index 2ff8244..07ae7a0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", "axios": "^1.6.0", + "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "mysql2": "^3.11.2", From 6ea74cf09d5b0e8e35cf3e2e09ada11726f1ae44 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 13 Sep 2024 12:15:00 -0700 Subject: [PATCH 07/18] [TM-1271] Use bcrypt to check if the password is valid. --- apps/user-service/src/auth/auth.controller.ts | 19 +++++++++++++------ apps/user-service/src/auth/auth.service.ts | 10 ++++++++-- .../decorators/json-api-response.decorator.ts | 1 + package-lock.json | 17 +++++++++++++++++ package.json | 2 ++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/apps/user-service/src/auth/auth.controller.ts b/apps/user-service/src/auth/auth.controller.ts index d629bfb..2c85efe 100644 --- a/apps/user-service/src/auth/auth.controller.ts +++ b/apps/user-service/src/auth/auth.controller.ts @@ -1,21 +1,28 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, HttpStatus, Post, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LoginRequest } from './dto/login-request.dto'; import { JsonApiResponse } from '../decorators/json-api-response.decorator'; import { LoginResponse } from './dto/login-response.dto'; +import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator'; +import { ApiOperation } from '@nestjs/swagger'; @Controller('auth') export class AuthController { constructor (protected readonly authService: AuthService) {} @Post('login') - @JsonApiResponse({ - status: 201, - description: 'Receive a JWT Token in exchange for login credentials', - dataType: LoginResponse - }) + @ApiOperation({ summary: 'Receive a JWT Token in exchange for login credentials' }) + @JsonApiResponse({ status: HttpStatus.OK, dataType: LoginResponse }) + @ApiException( + () => UnauthorizedException, + { description: 'Authentication failed.', template: { statusCode: '$status', message: '$description', } } + ) async login(@Body() { emailAddress, password }: LoginRequest): Promise { const token = await this.authService.login(emailAddress, password); + if (token == null) { + throw new UnauthorizedException(); + } + return { token }; } } diff --git a/apps/user-service/src/auth/auth.service.ts b/apps/user-service/src/auth/auth.service.ts index 85dee9e..44ca2f7 100644 --- a/apps/user-service/src/auth/auth.service.ts +++ b/apps/user-service/src/auth/auth.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { User } from '@terramatch-microservices/database'; +import bcrypt from 'bcryptjs'; @Injectable() export class AuthService { @@ -7,8 +8,13 @@ export class AuthService { // TODO: what additional fields do we need for JWT generation? This could simply be // User.findOneBy(), but it's nice not to have to pull the whole role from this fairly large // table - const { password: dbPassword } = await User.findOne({ select: { password: true }, where: { emailAddress } }); + const { password: passwordHash } = + await User.findOne({ select: { password: true }, where: { emailAddress } }) ?? {}; + if (passwordHash == null) return null; - return `Auth Service [${dbPassword}]`; + const passwordValid = await bcrypt.compare(password, passwordHash); + if (!passwordValid) return null; + + return `Auth Service [${passwordHash}]`; } } diff --git a/apps/user-service/src/decorators/json-api-response.decorator.ts b/apps/user-service/src/decorators/json-api-response.decorator.ts index 5b2bfb1..22b8fa6 100644 --- a/apps/user-service/src/decorators/json-api-response.decorator.ts +++ b/apps/user-service/src/decorators/json-api-response.decorator.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-types,@typescript-eslint/no-explicit-any */ import { ApiExtraModels, ApiResponse, ApiResponseOptions, getSchemaPath } from '@nestjs/swagger'; /** diff --git a/package-lock.json b/package-lock.json index d1019c7..a0fdcb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11", "@nestjs/common": "^10.0.2", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.2", @@ -40,6 +41,7 @@ "@swc-node/register": "~1.9.1", "@swc/core": "~1.5.7", "@swc/helpers": "~0.5.11", + "@types/bcryptjs": "^2.4.6", "@types/jest": "^29.5.12", "@types/node": "~18.16.9", "@typescript-eslint/eslint-plugin": "^7.16.0", @@ -3082,6 +3084,15 @@ "@module-federation/sdk": "0.6.3" } }, + "node_modules/@nanogiants/nestjs-swagger-api-exception-decorator": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/@nanogiants/nestjs-swagger-api-exception-decorator/-/nestjs-swagger-api-exception-decorator-1.6.11.tgz", + "integrity": "sha512-F2Jvj52BDFvKo0I5LFj+kSjwLQecqrs+ibDWokq6Xkod/wrT6gxGia1H/z7ENGk9XwwXfQL9rZt4W/+Vwp0ZhQ==", + "peerDependencies": { + "@nestjs/common": "^7.6.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/swagger": "^4.8.1 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", @@ -4485,6 +4496,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", diff --git a/package.json b/package.json index 07ae7a0..ad97efd 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": {}, "private": true, "dependencies": { + "@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11", "@nestjs/common": "^10.0.2", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.2", @@ -36,6 +37,7 @@ "@swc-node/register": "~1.9.1", "@swc/core": "~1.5.7", "@swc/helpers": "~0.5.11", + "@types/bcryptjs": "^2.4.6", "@types/jest": "^29.5.12", "@types/node": "~18.16.9", "@typescript-eslint/eslint-plugin": "^7.16.0", From 6af271f03cd84236975f252aaad5195f99cd8f90 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 13 Sep 2024 12:50:12 -0700 Subject: [PATCH 08/18] [TM-1271] Generate a JWT token. --- apps/user-service/src/app.module.ts | 13 +- apps/user-service/src/auth/auth.controller.ts | 6 +- apps/user-service/src/auth/auth.service.ts | 19 ++- libs/database/src/lib/database.module.ts | 1 - package-lock.json | 113 +++++++++++++++++- package.json | 1 + 6 files changed, 141 insertions(+), 12 deletions(-) diff --git a/apps/user-service/src/app.module.ts b/apps/user-service/src/app.module.ts index f48e465..6648a68 100644 --- a/apps/user-service/src/app.module.ts +++ b/apps/user-service/src/app.module.ts @@ -2,9 +2,20 @@ import { Module } from '@nestjs/common'; import { AuthController } from './auth/auth.controller'; import { AuthService } from './auth/auth.service'; import { DatabaseModule } from '@terramatch-microservices/database'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ - imports: [DatabaseModule], + imports: [ + DatabaseModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + }), + }) + ], controllers: [AuthController], providers: [AuthService], }) diff --git a/apps/user-service/src/auth/auth.controller.ts b/apps/user-service/src/auth/auth.controller.ts index 2c85efe..a5101a3 100644 --- a/apps/user-service/src/auth/auth.controller.ts +++ b/apps/user-service/src/auth/auth.controller.ts @@ -8,7 +8,7 @@ import { ApiOperation } from '@nestjs/swagger'; @Controller('auth') export class AuthController { - constructor (protected readonly authService: AuthService) {} + constructor (private readonly authService: AuthService) {} @Post('login') @ApiOperation({ summary: 'Receive a JWT Token in exchange for login credentials' }) @@ -20,6 +20,10 @@ export class AuthController { async login(@Body() { emailAddress, password }: LoginRequest): Promise { const token = await this.authService.login(emailAddress, password); if (token == null) { + // there are multiple reasons for the token to be null (bad email address, wrong password), + // but we don't want to report on the specifics because it opens an attack vector: if we + // report that an email address isn't valid, that lets an attacker know which email addresses + // _are_ valid in our system. throw new UnauthorizedException(); } diff --git a/apps/user-service/src/auth/auth.service.ts b/apps/user-service/src/auth/auth.service.ts index 44ca2f7..5f001a7 100644 --- a/apps/user-service/src/auth/auth.service.ts +++ b/apps/user-service/src/auth/auth.service.ts @@ -1,20 +1,27 @@ import { Injectable } from '@nestjs/common'; import { User } from '@terramatch-microservices/database'; import bcrypt from 'bcryptjs'; +import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { + constructor (private readonly jwtService: JwtService) {} + async login(emailAddress: string, password: string) { - // TODO: what additional fields do we need for JWT generation? This could simply be - // User.findOneBy(), but it's nice not to have to pull the whole role from this fairly large - // table - const { password: passwordHash } = - await User.findOne({ select: { password: true }, where: { emailAddress } }) ?? {}; + const { id, password: passwordHash } = + await User.findOne({ select: { id: true, password: true }, where: { emailAddress } }) ?? {}; if (passwordHash == null) return null; const passwordValid = await bcrypt.compare(password, passwordHash); if (!passwordValid) return null; - return `Auth Service [${passwordHash}]`; + return await this.jwtService.signAsync({ + sub: id, + // sha1 hash of 'App\\Models\\V2\\User'. Needed for the PHP Backend to + // successfully find a user based on the JWT token generated here. + prv: '71af459e0508a77644680012c8d33882322544be' + }, { + expiresIn: '12h' + }); } } diff --git a/libs/database/src/lib/database.module.ts b/libs/database/src/lib/database.module.ts index d5d4854..fe6abb5 100644 --- a/libs/database/src/lib/database.module.ts +++ b/libs/database/src/lib/database.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmConfigService } from './typeorm-config.service'; -import { User } from './entities/user.entity'; @Module({ imports: [ diff --git a/package-lock.json b/package-lock.json index a0fdcb0..f6682b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@nestjs/common": "^10.0.2", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.2", + "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.0.2", "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", @@ -3201,6 +3202,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", @@ -4634,6 +4647,14 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4643,8 +4664,7 @@ "node_modules/@types/node": { "version": "18.16.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.20.tgz", - "integrity": "sha512-nL54VfDjThdP2UXJXZao5wp76CDiDw4zSRO8d4Tk7UgDqNKGKVEQB0/t3ti63NS+YNNkIQDvwEAF04BO+WYu7Q==", - "devOptional": true + "integrity": "sha512-nL54VfDjThdP2UXJXZao5wp76CDiDw4zSRO8d4Tk7UgDqNKGKVEQB0/t3ti63NS+YNNkIQDvwEAF04BO+WYu7Q==" }, "node_modules/@types/node-forge": { "version": "1.3.11", @@ -5990,6 +6010,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7411,6 +7436,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -10468,6 +10501,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "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.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -10820,6 +10893,36 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -10832,6 +10935,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -13243,7 +13351,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "bin": { "semver": "bin/semver.js" }, diff --git a/package.json b/package.json index ad97efd..5723831 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@nestjs/common": "^10.0.2", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.2", + "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.0.2", "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", From e8a95a96f8a6d77c9d6fafc5f9f07bf5bb3bcafe Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 13 Sep 2024 13:05:49 -0700 Subject: [PATCH 09/18] [TM-1271] Set the 'last_logged_in_at' timestamp. --- apps/user-service/src/auth/auth.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/user-service/src/auth/auth.service.ts b/apps/user-service/src/auth/auth.service.ts index 5f001a7..094a6b4 100644 --- a/apps/user-service/src/auth/auth.service.ts +++ b/apps/user-service/src/auth/auth.service.ts @@ -15,6 +15,8 @@ export class AuthService { const passwordValid = await bcrypt.compare(password, passwordHash); if (!passwordValid) return null; + await User.update({ id }, { lastLoggedInAt: () => 'now()' }); + return await this.jwtService.signAsync({ sub: id, // sha1 hash of 'App\\Models\\V2\\User'. Needed for the PHP Backend to From 58bd4d9354cf062d41e73e5c269516170701d125 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 13 Sep 2024 13:58:41 -0700 Subject: [PATCH 10/18] [TM-1271] Most endpoints return 200, so make that the default. --- apps/user-service/src/auth/auth.controller.ts | 4 ++-- .../src/decorators/json-api-response.decorator.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/user-service/src/auth/auth.controller.ts b/apps/user-service/src/auth/auth.controller.ts index a5101a3..0fcb36e 100644 --- a/apps/user-service/src/auth/auth.controller.ts +++ b/apps/user-service/src/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, HttpStatus, Post, UnauthorizedException } from '@nestjs/common'; +import { Body, Controller, Post, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LoginRequest } from './dto/login-request.dto'; import { JsonApiResponse } from '../decorators/json-api-response.decorator'; @@ -12,7 +12,7 @@ export class AuthController { @Post('login') @ApiOperation({ summary: 'Receive a JWT Token in exchange for login credentials' }) - @JsonApiResponse({ status: HttpStatus.OK, dataType: LoginResponse }) + @JsonApiResponse({ dataType: LoginResponse }) @ApiException( () => UnauthorizedException, { description: 'Authentication failed.', template: { statusCode: '$status', message: '$description', } } diff --git a/apps/user-service/src/decorators/json-api-response.decorator.ts b/apps/user-service/src/decorators/json-api-response.decorator.ts index 22b8fa6..1a18bea 100644 --- a/apps/user-service/src/decorators/json-api-response.decorator.ts +++ b/apps/user-service/src/decorators/json-api-response.decorator.ts @@ -1,14 +1,16 @@ /* eslint-disable @typescript-eslint/ban-types,@typescript-eslint/no-explicit-any */ import { ApiExtraModels, ApiResponse, ApiResponseOptions, getSchemaPath } from '@nestjs/swagger'; +import { HttpStatus } from '@nestjs/common'; /** * Decorator to simplify wrapping the response type from a controller method with the JSON API * response structure. Applies the ApiExtraModels and ApiResponse decorators. */ export function JsonApiResponse(options: ApiResponseOptions & { dataType: Function }): MethodDecorator & ClassDecorator { - const { dataType, ...rest } = options; + const { dataType, status, ...rest } = options; const apiResponseOptions = { ...rest, + status: status ?? HttpStatus.OK, schema: { type: "object", properties: { From 511ed2902bc181787cfd4e30b4630f6471e8cc8a Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 13 Sep 2024 14:47:05 -0700 Subject: [PATCH 11/18] [TM-1271] Require that JSON API responses always have a type and ID. We'll want that for the FE layer. --- .github/workflows/ci.yml | 6 +----- apps/user-service/src/auth/auth.controller.ts | 8 ++++---- apps/user-service/src/auth/auth.service.ts | 3 ++- apps/user-service/src/auth/dto/login-response.dto.ts | 11 +++++++++-- .../src/decorators/json-api-response.decorator.ts | 7 +++++-- .../src/interfaces/json-api-data-dto.interface.ts | 4 ++++ 6 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 apps/user-service/src/interfaces/json-api-data-dto.interface.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6c7b35..a4d6a76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,7 @@ name: CI on: - push: -# disabled for now - branches: - - none -# pull_request: + pull_request: permissions: actions: read diff --git a/apps/user-service/src/auth/auth.controller.ts b/apps/user-service/src/auth/auth.controller.ts index 0fcb36e..c81d341 100644 --- a/apps/user-service/src/auth/auth.controller.ts +++ b/apps/user-service/src/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Post, UnauthorizedException } from '@nestjs/common'; +import { Body, Controller, HttpStatus, Post, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LoginRequest } from './dto/login-request.dto'; import { JsonApiResponse } from '../decorators/json-api-response.decorator'; @@ -12,13 +12,13 @@ export class AuthController { @Post('login') @ApiOperation({ summary: 'Receive a JWT Token in exchange for login credentials' }) - @JsonApiResponse({ dataType: LoginResponse }) + @JsonApiResponse({ status: HttpStatus.CREATED, dataType: LoginResponse }) @ApiException( () => UnauthorizedException, { description: 'Authentication failed.', template: { statusCode: '$status', message: '$description', } } ) async login(@Body() { emailAddress, password }: LoginRequest): Promise { - const token = await this.authService.login(emailAddress, password); + const { token, userId } = await this.authService.login(emailAddress, password) ?? {} if (token == null) { // there are multiple reasons for the token to be null (bad email address, wrong password), // but we don't want to report on the specifics because it opens an attack vector: if we @@ -27,6 +27,6 @@ export class AuthController { throw new UnauthorizedException(); } - return { token }; + return { type: 'logins', id: `${userId}`, token }; } } diff --git a/apps/user-service/src/auth/auth.service.ts b/apps/user-service/src/auth/auth.service.ts index 094a6b4..3fcad8f 100644 --- a/apps/user-service/src/auth/auth.service.ts +++ b/apps/user-service/src/auth/auth.service.ts @@ -17,7 +17,7 @@ export class AuthService { await User.update({ id }, { lastLoggedInAt: () => 'now()' }); - return await this.jwtService.signAsync({ + const token = await this.jwtService.signAsync({ sub: id, // sha1 hash of 'App\\Models\\V2\\User'. Needed for the PHP Backend to // successfully find a user based on the JWT token generated here. @@ -25,5 +25,6 @@ export class AuthService { }, { expiresIn: '12h' }); + return { token, userId: id }; } } diff --git a/apps/user-service/src/auth/dto/login-response.dto.ts b/apps/user-service/src/auth/dto/login-response.dto.ts index ed33aaa..098fb0f 100644 --- a/apps/user-service/src/auth/dto/login-response.dto.ts +++ b/apps/user-service/src/auth/dto/login-response.dto.ts @@ -1,6 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; +import { JsonApiDataDto } from '../../interfaces/json-api-data-dto.interface'; -export class LoginResponse { - @ApiProperty({ description: 'JWT token for use in future authenticated requests to the API.' }) +export class LoginResponse implements JsonApiDataDto { + @ApiProperty({ example: 'logins' }) + type: string; + + @ApiProperty({ description: 'The ID of the user associated with this login', example: '1234' }) + id: string; + + @ApiProperty({ description: 'JWT token for use in future authenticated requests to the API.', example: '' }) token: string; } diff --git a/apps/user-service/src/decorators/json-api-response.decorator.ts b/apps/user-service/src/decorators/json-api-response.decorator.ts index 1a18bea..740383c 100644 --- a/apps/user-service/src/decorators/json-api-response.decorator.ts +++ b/apps/user-service/src/decorators/json-api-response.decorator.ts @@ -1,12 +1,15 @@ -/* eslint-disable @typescript-eslint/ban-types,@typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { ApiExtraModels, ApiResponse, ApiResponseOptions, getSchemaPath } from '@nestjs/swagger'; import { HttpStatus } from '@nestjs/common'; +import { JsonApiDataDto } from '../interfaces/json-api-data-dto.interface'; /** * Decorator to simplify wrapping the response type from a controller method with the JSON API * response structure. Applies the ApiExtraModels and ApiResponse decorators. */ -export function JsonApiResponse(options: ApiResponseOptions & { dataType: Function }): MethodDecorator & ClassDecorator { +export function JsonApiResponse( + options: ApiResponseOptions & { dataType: new () => C } +): MethodDecorator & ClassDecorator { const { dataType, status, ...rest } = options; const apiResponseOptions = { ...rest, diff --git a/apps/user-service/src/interfaces/json-api-data-dto.interface.ts b/apps/user-service/src/interfaces/json-api-data-dto.interface.ts new file mode 100644 index 0000000..f7ce1e6 --- /dev/null +++ b/apps/user-service/src/interfaces/json-api-data-dto.interface.ts @@ -0,0 +1,4 @@ +export interface JsonApiDataDto { + type: string; + id: string; +} From 6ba380f072e00499b926139754f0b9ad12fe85cf Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Mon, 16 Sep 2024 15:04:46 -0700 Subject: [PATCH 12/18] [TM-1271] Spec out the auth service. --- .env.local.sample | 8 ++ README.md | 14 +++ .../src/auth/auth.service.spec.ts | 77 +++++++++++++- libs/database/src/index.ts | 1 + libs/database/src/lib/entities/user.entity.ts | 100 ++++++++++-------- .../database/src/lib/entities/user.factory.ts | 23 ++++ .../src/lib/typeorm-config.service.ts | 1 + package-lock.json | 39 +++++++ package.json | 3 + 9 files changed, 216 insertions(+), 50 deletions(-) create mode 100644 libs/database/src/lib/entities/user.factory.ts diff --git a/.env.local.sample b/.env.local.sample index 3493036..a00675d 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -1,3 +1,11 @@ NODE_ENV=development PHP_PROXY_TARGET=http://host.docker.internal:8080/api USER_SERVICE_PROXY_TARGET=http://host.docker.internal:4010 + +DB_HOST=localhost +DB_PORT=3360 +DB_DATABASE=wri_restoration_marketplace_api +DB_USERNAME=wri +DB_PASSWORD=wri + +JWT_SECRET=qu3sep4GKdbg6PiVPCKLKljHukXALorq6nLHDBOCSwvs6BrgE6zb8gPmZfrNspKt diff --git a/README.md b/README.md index d30cef9..fdce0d1 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,17 @@ Repository for the Microservices API backend of the TerraMatch service # Deployment TBD. The ApiGateway has been tested to be at least functional on AWS. Tooling around deployment will be handled in a future ticket. + +# Database work +For now, Laravel is the source of truth for all things related to the DB schema. As such, TypeORM is not allowed to modify the +schema, and is expected to interface with exactly the schema that is managed by Laravel. This note is included in user.entity.ts, +and should hold true for all models created in this codebase until this codebase can take over as the source of truth for DB +schema: +``` +// Note: this has some additional typing information (like width: 1 on bools and type: timestamps on +// CreateDateColumn) to make the types generated here match what is generated by Laravel exactly. +// At this time, we want TypeORM to expect exactly the same types that PHP uses by default. Tested +// by checking what schema gets generated in the test database against the real DB during unit +// test runs (the only time we let TypeORM modify the DB schema). +``` + diff --git a/apps/user-service/src/auth/auth.service.spec.ts b/apps/user-service/src/auth/auth.service.spec.ts index 800ab66..169c8e4 100644 --- a/apps/user-service/src/auth/auth.service.spec.ts +++ b/apps/user-service/src/auth/auth.service.spec.ts @@ -1,18 +1,89 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; +import { JwtService } from '@nestjs/jwt'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { User } from '@terramatch-microservices/database'; +import { FactoryGirl, TypeOrmRepositoryAdapter } from 'factory-girl-ts'; +import { DataSource } from 'typeorm'; +import { UserFactory } from '@terramatch-microservices/database'; +import bcrypt from 'bcryptjs'; + +const dataSource = new DataSource({ + type: 'mariadb', + host: 'localhost', + port: 3360, + username: 'wri', + password: 'wri', + // TODO: script to create DB. Going to need a docker container on github actions + database: 'terramatch_microservices_test', + timezone: 'Z', + entities: [User], + synchronize: true, +}); describe('AuthService', () => { let service: AuthService; + let jwtService: DeepMocked; + + beforeAll(async () => { + FactoryGirl.setAdapter(new TypeOrmRepositoryAdapter(dataSource)); + + await dataSource.initialize(); + await dataSource.getRepository(User).delete({}); + }) + + afterAll(async () => { + await dataSource.driver.disconnect(); + }) beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [AuthService], + providers: [ + AuthService, + { provide: JwtService, useValue: jwtService = createMock() } + ], }).compile(); service = module.get(AuthService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + afterEach(() => { + jest.restoreAllMocks(); + }) + + it('should return null with invalid email', async () => { + jest.spyOn(User, 'findOne').mockImplementation(() => Promise.resolve(null)); + expect(await service.login('fake@foo.bar', 'asdfasdfsadf')).toBeNull() + }) + + it('should return null with an invalid password', async () => { + const { emailAddress } = await UserFactory.create({ password: 'fakepasswordhash' }); + expect(await service.login(emailAddress, 'fakepassword')).toBeNull(); + }) + + it('should return a token and id with a valid password', async () => { + const { id, emailAddress } = await UserFactory.create({ password: 'fakepasswordhash' }); + jest.spyOn(bcrypt, 'compare').mockImplementation(() => Promise.resolve(true)); + + const token = 'fake jwt token'; + jwtService.signAsync.mockReturnValue(Promise.resolve(token)); + + const result = await service.login(emailAddress, 'fakepassword'); + + expect(jwtService.signAsync).toHaveBeenCalled(); + expect(result.token).toBe(token); + expect(result.userId).toBe(id); }); + + it('should update the last logged in date on the user', async () => { + const user = await UserFactory.create({ password: 'fakepasswordhash' }); + jest.spyOn(bcrypt, 'compare').mockImplementation(() => Promise.resolve(true)); + jwtService.signAsync.mockReturnValue(Promise.resolve('fake jwt token')); + + await service.login(user.emailAddress, 'fakepassword'); + + const { lastLoggedInAt } = user; + await user.reload(); + expect(lastLoggedInAt).not.toBe(user.lastLoggedInAt); + }) }); diff --git a/libs/database/src/index.ts b/libs/database/src/index.ts index 933657d..27f5a84 100644 --- a/libs/database/src/index.ts +++ b/libs/database/src/index.ts @@ -1,2 +1,3 @@ export * from './lib/database.module'; export * from './lib/entities/user.entity'; +export * from './lib/entities/user.factory'; diff --git a/libs/database/src/lib/entities/user.entity.ts b/libs/database/src/lib/entities/user.entity.ts index 48ceee7..8cfbc65 100644 --- a/libs/database/src/lib/entities/user.entity.ts +++ b/libs/database/src/lib/entities/user.entity.ts @@ -3,91 +3,97 @@ import { Column, CreateDateColumn, DeleteDateColumn, - Entity, + Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +// Note: this has some additional typing information (like width: 1 on bools and type: timestamps on +// CreateDateColumn) to make the types generated here match what is generated by Laravel exactly. +// At this time, we want TypeORM to expect exactly the same types that PHP uses by default. Tested +// by checking what schema gets generated in the test database against the real DB during unit +// test runs (the only time we let TypeORM modify the DB schema). @Entity({ name: 'users' }) export class User extends BaseEntity { - @PrimaryGeneratedColumn() + @PrimaryGeneratedColumn({ type: 'bigint', unsigned: true }) id: number; - @Column('uuid') + // There are many rows in the prod DB without a UUID assigned, so this cannot be a unique + // index until that is fixed. + @Column({ type: 'char', width: 36 }) + @Index() uuid: string; - @CreateDateColumn({ name: 'created_at' }) + @CreateDateColumn({ type: 'timestamp', name: 'created_at' }) createdAt: Date; - @UpdateDateColumn({ name: 'updated_at' }) + @UpdateDateColumn({ type: 'timestamp', name: 'updated_at' }) updatedAt: Date; - @DeleteDateColumn({ name: 'deleted_at' }) + @DeleteDateColumn({ type: 'timestamp', name: 'deleted_at' }) deletedAt: Date; // TODO: relate this to the organisations table - // Note: this is a bigint(20) unsigned according to describe users, but TypeORM claims that - // unsigned big int isn't supported by MariaDB. - @Column('bigint', { name: 'organisation_id' }) - organisationId: number; + @Column({ type: 'bigint', name: 'organisation_id', nullable: true, unsigned: true }) + organisationId: number | null; - @Column({ name: 'first_name' }) - firstName: string; + @Column({ name: 'first_name', nullable: true }) + firstName: string | null; - @Column({ name: 'last_name' }) - lastName: string; + @Column({ name: 'last_name', nullable: true }) + lastName: string | null; - @Column({ name: 'email_address' }) + @Column({ name: 'email_address', unique: true }) emailAddress: string; - @Column() - password: string; + @Column({ nullable: true }) + password: string | null; - @Column({ name: 'email_address_verified_at' }) - emailAddressVerifiedAt: Date; + @Column({ name: 'email_address_verified_at', nullable: true }) + emailAddressVerifiedAt: Date | null; - @Column({ name: 'last_logged_in_at' }) - lastLoggedInAt: Date; + @Column({ name: 'last_logged_in_at', nullable: true }) + lastLoggedInAt: Date | null; - @Column({ name: 'job_role' }) - jobRole: string; + @Column({ name: 'job_role', nullable: true }) + jobRole: string | null; - @Column() - facebook: string; + @Column({ nullable: true }) + facebook: string | null; - @Column() - twitter: string; + @Column({ nullable: true }) + twitter: string | null; - @Column() - linkedin: string; + @Column({ nullable: true }) + linkedin: string | null; - @Column() - instagram: string; + @Column({ nullable: true }) + instagram: string | null; - @Column() - avatar: string; + @Column({ nullable: true }) + avatar: string | null; - @Column({ name: 'phone_number' }) - phoneNumber: string; + @Column({ name: 'phone_number', nullable: true }) + phoneNumber: string | null; - @Column({ name: 'whatsapp_phone' }) - whatsappPhone: string; + @Column({ name: 'whatsapp_phone', nullable: true }) + whatsappPhone: string | null; - @Column('bool', { name: 'is_subscribed' }) + @Column({ width: 1, name: 'is_subscribed', default: true }) isSubscribed: boolean; - @Column('bool', { name: 'has_consented' }) + @Column({ width: 1, name: 'has_consented', default: true }) hasConsented: boolean; - @Column() - banners: string; + @Column({ nullable: true }) + banners: string | null; - @Column({ name: 'api_key' }) - apiKey: string; + @Column({ name: 'api_key', nullable: true }) + apiKey: string | null; - @Column() - country: string; + @Column({ nullable: true }) + country: string | null; - @Column() - program: string; + @Column({ nullable: true }) + program: string | null; } diff --git a/libs/database/src/lib/entities/user.factory.ts b/libs/database/src/lib/entities/user.factory.ts new file mode 100644 index 0000000..c8beea2 --- /dev/null +++ b/libs/database/src/lib/entities/user.factory.ts @@ -0,0 +1,23 @@ +import { FactoryGirl } from 'factory-girl-ts'; +import { User } from './user.entity'; +import { faker } from '@faker-js/faker'; + +// TODO: generate correctly hashed passwords. This will be easily accomplished once user signup +// has been implemented in this codebase. +export const UserFactory = FactoryGirl.define(User, async () => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + emailAddress: await generateUniqueEmail(), + emailAddressVerifiedAt: new Date(), + uuid: crypto.randomUUID(), +})); + +async function generateUniqueEmail() { + let emailAddress = faker.internet.email(); + + while (await User.findOneBy({ emailAddress }) != null) { + emailAddress = faker.internet.email(); + } + + return emailAddress; +} diff --git a/libs/database/src/lib/typeorm-config.service.ts b/libs/database/src/lib/typeorm-config.service.ts index e9121fc..935ca62 100644 --- a/libs/database/src/lib/typeorm-config.service.ts +++ b/libs/database/src/lib/typeorm-config.service.ts @@ -15,6 +15,7 @@ export class TypeOrmConfigService implements TypeOrmOptionsFactory { username: this.configService.get('DB_USERNAME'), password: this.configService.get('DB_PASSWORD'), database: this.configService.get('DB_DATABASE'), + timezone: 'Z', entities: [User], }; } diff --git a/package-lock.json b/package-lock.json index f6682b2..9a765cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,8 @@ "typeorm": "^0.3.20" }, "devDependencies": { + "@faker-js/faker": "^9.0.0", + "@golevelup/ts-jest": "^0.5.5", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.0.2", "@nx/eslint": "19.7.2", @@ -49,6 +51,7 @@ "@typescript-eslint/parser": "^7.16.0", "eslint": "~8.57.0", "eslint-config-prettier": "^9.0.0", + "factory-girl-ts": "^2.3.1", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", "nx": "19.7.2", @@ -2262,6 +2265,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", + "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@golevelup/ts-jest": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.5.5.tgz", + "integrity": "sha512-x1kAFZ6ADPpwl6rauyJY6uP3OdTXA+BHb2S6nK/dpQcoAyC1gAPlb7kBTFfzvxm8yaoIieaR/638kVZSK6GxtQ==", + "dev": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -8099,6 +8124,20 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/factory-girl-ts": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/factory-girl-ts/-/factory-girl-ts-2.3.1.tgz", + "integrity": "sha512-JnE3kGIfGGBfJrSekBnYN1H9dSEnRsFw4+Oy+Z1LY9GplTPDg0sHsNIY0Ya+PT9ueSOu5xPNAyLoCHWr2vKlag==", + "dev": true, + "dependencies": { + "class-transformer": "0.5.1", + "deepmerge": "^4.3.1", + "lodash": "4.17.21" + }, + "engines": { + "node": ">= 18.16.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 5723831..0c42186 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "typeorm": "^0.3.20" }, "devDependencies": { + "@faker-js/faker": "^9.0.0", + "@golevelup/ts-jest": "^0.5.5", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.0.2", "@nx/eslint": "19.7.2", @@ -45,6 +47,7 @@ "@typescript-eslint/parser": "^7.16.0", "eslint": "~8.57.0", "eslint-config-prettier": "^9.0.0", + "factory-girl-ts": "^2.3.1", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", "nx": "19.7.2", From 172b4d3fef08ce7d351b6e845e771482388d6cdf Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Mon, 16 Sep 2024 15:44:06 -0700 Subject: [PATCH 13/18] [TM-1271] Spec out the controller. --- .../src/auth/auth.controller.spec.ts | 30 +++++++++++++++++-- .../src/auth/auth.service.spec.ts | 2 +- jest.preset.js | 13 +++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/apps/user-service/src/auth/auth.controller.spec.ts b/apps/user-service/src/auth/auth.controller.spec.ts index 27a31e6..722f469 100644 --- a/apps/user-service/src/auth/auth.controller.spec.ts +++ b/apps/user-service/src/auth/auth.controller.spec.ts @@ -1,18 +1,42 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { UnauthorizedException } from '@nestjs/common'; describe('AuthController', () => { let controller: AuthController; + let authService: DeepMocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [AuthController], + providers: [ + { provide: AuthService, useValue: authService = createMock() }, + ], }).compile(); controller = module.get(AuthController); }); - it('should be defined', () => { - expect(controller).toBeDefined(); - }); + afterEach(() => { + jest.restoreAllMocks(); + }) + + it('should throw if creds are invalid', async () => { + authService.login.mockResolvedValue(null); + + await expect(() => controller.login({ emailAddress: 'foo@bar.com', password: 'asdfasdfasdf' })) + .rejects + .toThrow(UnauthorizedException) + }) + + it('returns a token if creds are valid', async () => { + const token = 'fake jwt token'; + const userId = 123; + authService.login.mockResolvedValue({ token, userId }) + + const result = await controller.login({ emailAddress: 'foo@bar.com', password: 'asdfasdfasdf' }); + expect(result).toEqual({ type: 'logins', token, id: `${userId}` }) + }) }); diff --git a/apps/user-service/src/auth/auth.service.spec.ts b/apps/user-service/src/auth/auth.service.spec.ts index 169c8e4..4fc99e6 100644 --- a/apps/user-service/src/auth/auth.service.spec.ts +++ b/apps/user-service/src/auth/auth.service.spec.ts @@ -78,7 +78,7 @@ describe('AuthService', () => { it('should update the last logged in date on the user', async () => { const user = await UserFactory.create({ password: 'fakepasswordhash' }); jest.spyOn(bcrypt, 'compare').mockImplementation(() => Promise.resolve(true)); - jwtService.signAsync.mockReturnValue(Promise.resolve('fake jwt token')); + jwtService.signAsync.mockResolvedValue('fake jwt token'); await service.login(user.emailAddress, 'fakepassword'); diff --git a/jest.preset.js b/jest.preset.js index f078ddc..2cc06ad 100644 --- a/jest.preset.js +++ b/jest.preset.js @@ -1,3 +1,14 @@ const nxPreset = require('@nx/jest/preset').default; -module.exports = { ...nxPreset }; +module.exports = { + ...nxPreset, + + coverageThreshold: { + global: { + branches: 85, + functions: 95, + lines: 95, + statements: 95, + } + } +} From cc9290ba3a58db38227cd2688bf1da92b3e941d2 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Mon, 16 Sep 2024 16:03:50 -0700 Subject: [PATCH 14/18] [TM-1271] Set up lint / test / build CI action --- .github/workflows/{ci.yml => pull-request.yml} | 11 +++++++---- README.md | 3 +++ docker-compose.yml | 15 +++++++++++++++ docker/mariadb.Dockerfile | 3 +++ docker/override.cnf | 10 ++++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) rename .github/workflows/{ci.yml => pull-request.yml} (69%) create mode 100644 docker-compose.yml create mode 100644 docker/mariadb.Dockerfile create mode 100644 docker/override.cnf diff --git a/.github/workflows/ci.yml b/.github/workflows/pull-request.yml similarity index 69% rename from .github/workflows/ci.yml rename to .github/workflows/pull-request.yml index a4d6a76..138853e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/pull-request.yml @@ -15,11 +15,17 @@ jobs: with: fetch-depth: 0 + - uses: KengoTODA/actions-setup-docker-compose@v1 + with: + version: '2.29.1' + # This enables task distribution via Nx Cloud # Run this command as early as possible, before dependencies are installed # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build" + - run: docker-compose up -d + # Cache node_modules - uses: actions/setup-node@v4 with: @@ -29,7 +35,4 @@ jobs: - run: npm ci --legacy-peer-deps - uses: nrwl/nx-set-shas@v4 - # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud - # - run: npx nx-cloud record -- echo Hello World - # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected - - run: npx nx affected -t lint test build + - run: npx nx affected -t lint 'test --coverage --passWithNoTests' build diff --git a/README.md b/README.md index fdce0d1..b682664 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,6 @@ schema: // test runs (the only time we let TypeORM modify the DB schema). ``` +This codebase connects to the database running in the `wri-terramatch-api` docker container. The docker-compose +file included in this repo is used only for setting up the database needed for running unit tests in Github Actions. + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3f99ac5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3" +services: + mariadb: + build: + context: "." + dockerfile: "docker/mariadb.Dockerfile" + ports: + - "3360:3306" + networks: + - "default" + environment: + MYSQL_ROOT_PASSWORD: "root" + MYSQL_DATABASE: "terramatch_microservices_test" + MYSQL_USER: "wri" + MYSQL_PASSWORD: "wri" diff --git a/docker/mariadb.Dockerfile b/docker/mariadb.Dockerfile new file mode 100644 index 0000000..0301945 --- /dev/null +++ b/docker/mariadb.Dockerfile @@ -0,0 +1,3 @@ +# MARIADB +FROM mariadb:10.3 AS mariadb +COPY docker/override.cnf /etc/mysql/mariadb.conf.d/override.cnf diff --git a/docker/override.cnf b/docker/override.cnf new file mode 100644 index 0000000..4b75bb5 --- /dev/null +++ b/docker/override.cnf @@ -0,0 +1,10 @@ +[client] +default-character-set=utf8mb4 + +[mysql] +default-character-set=utf8mb4 + +[mysqld] +collation-server = utf8mb4_unicode_520_ci +init-connect='SET NAMES utf8mb4' +character-set-server = utf8mb4 From 8eb257de4548ed4d28a02474767c2f3b49dcd804 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Mon, 16 Sep 2024 16:19:44 -0700 Subject: [PATCH 15/18] [TM-1271] Add a note about setting up local testing. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index b682664..e777c8c 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,7 @@ schema: This codebase connects to the database running in the `wri-terramatch-api` docker container. The docker-compose file included in this repo is used only for setting up the database needed for running unit tests in Github Actions. +To set up the local testing database, run these two commands in the `wri-terramatch-api` directory with the docker container running: +* `echo "grant all on terramatch_microservices_test to 'wri'@'%';" | dc exec -T mariadb mysql -h localhost -u root -proot ` +* `echo "grant all on terramatch_microservices_test.* to 'wri'@'%';" | dc exec -T mariadb mysql -h localhost -u root -proot` + From 05e3c01c6c3d6251fbcd428ca4e659538b7dd899 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Wed, 18 Sep 2024 11:27:53 -0700 Subject: [PATCH 16/18] [TM-1271] Add integration with the NX access token. --- .github/workflows/pull-request.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 138853e..1cf203a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,4 +1,7 @@ -name: CI +name: Pull Request + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} on: pull_request: From ea80b99d0b012a58a5b2fa2d2d4ef9aa12f23c92 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Wed, 18 Sep 2024 11:31:48 -0700 Subject: [PATCH 17/18] [TM-1271] Turn off distributed execution. --- .github/workflows/pull-request.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1cf203a..9f5b963 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -38,4 +38,8 @@ jobs: - run: npm ci --legacy-peer-deps - uses: nrwl/nx-set-shas@v4 - - run: npx nx affected -t lint 'test --coverage --passWithNoTests' build + # Distributed execution only works with cacheable builds. The Api Gateway build is not currently + # cacheable. Since the codebase is currently small, we can get away without distribution, but once + # it grows, we'll want to look into what it will take to make the api gateway build cacheable and remove + # NX_CLOUD_DISTRIBUTED_EXECUTION=false from this command. + - run: NX_CLOUD_DISTRIBUTED_EXECUTION=false npx nx affected -t lint 'test --coverage --passWithNoTests' build From ee3982d84a6ecdd3bafbe378b394e898ab7e21c4 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Wed, 18 Sep 2024 12:09:23 -0700 Subject: [PATCH 18/18] [TM-1271] Need to install deps for the gateway too. --- .github/workflows/pull-request.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 9f5b963..78b8a18 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -35,7 +35,8 @@ jobs: node-version: 20 cache: 'npm' - - run: npm ci --legacy-peer-deps + - run: npm ci --legacy-peer-deps && (cd apps/api-gateway; npm ci) && (cd apps/api-gateway/lambda/local-proxy; npm ci) + - uses: nrwl/nx-set-shas@v4 # Distributed execution only works with cacheable builds. The Api Gateway build is not currently