From a1bd6ff93928f72e41662b6f73bebda1788f120e Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Wed, 28 Jun 2023 22:53:13 +0300 Subject: [PATCH 01/17] feat: add user data service --- apps/auth-e2e/.eslintrc.json | 10 + apps/auth-e2e/jest.config.ts | 19 ++ apps/auth-e2e/project.json | 22 ++ apps/auth-e2e/src/auth/auth.spec.ts | 10 + apps/auth-e2e/src/support/global-setup.ts | 10 + apps/auth-e2e/src/support/global-teardown.ts | 7 + apps/auth-e2e/src/support/test-setup.ts | 10 + apps/auth-e2e/tsconfig.json | 13 + apps/auth-e2e/tsconfig.spec.json | 9 + apps/auth/.eslintrc.json | 18 ++ apps/auth/jest.config.ts | 11 + apps/auth/project.json | 64 +++++ apps/auth/src/app/app.controller.spec.ts | 22 ++ apps/auth/src/app/app.controller.ts | 13 + apps/auth/src/app/app.module.ts | 11 + apps/auth/src/app/app.service.spec.ts | 21 ++ apps/auth/src/app/app.service.ts | 8 + apps/auth/src/assets/.gitkeep | 0 apps/auth/src/main.ts | 22 ++ apps/auth/tsconfig.app.json | 12 + apps/auth/tsconfig.json | 16 ++ apps/auth/tsconfig.spec.json | 14 ++ apps/auth/webpack.config.js | 8 + apps/frontend/src/assets/CTF.jpg | Bin 0 -> 119264 bytes apps/user-data-e2e/.eslintrc.json | 10 + apps/user-data-e2e/jest.config.ts | 19 ++ apps/user-data-e2e/project.json | 22 ++ .../user-data-e2e/src/support/global-setup.ts | 10 + .../src/support/global-teardown.ts | 7 + apps/user-data-e2e/src/support/test-setup.ts | 10 + .../src/user-data/user-data.spec.ts | 10 + apps/user-data-e2e/tsconfig.json | 13 + apps/user-data-e2e/tsconfig.spec.json | 9 + apps/user-data/.ASC_MANIFEST | 13 + apps/user-data/.env.example | 8 + apps/user-data/.eslintrc.json | 18 ++ apps/user-data/.gitignore | 3 + apps/user-data/Dockerfile | 32 +++ apps/user-data/LICENSE | 11 + apps/user-data/jest.config.ts | 11 + apps/user-data/prisma/schema.prisma | 53 +++++ apps/user-data/project.json | 64 +++++ apps/user-data/serving/.gitignore | 225 ++++++++++++++++++ apps/user-data/serving/database.env.example | 3 + apps/user-data/serving/docker-compose.yaml | 34 +++ apps/user-data/src/app/app.controller.spec.ts | 22 ++ apps/user-data/src/app/app.controller.ts | 13 + apps/user-data/src/app/app.module.ts | 14 ++ .../src/app/service/app.service.spec.ts | 21 ++ apps/user-data/src/app/service/app.service.ts | 8 + apps/user-data/src/assets/.gitkeep | 0 .../src/email/email.controller.spec.ts | 18 ++ apps/user-data/src/email/email.controller.ts | 53 +++++ apps/user-data/src/email/email.module.ts | 12 + .../src/email/service/email.service.spec.ts | 18 ++ .../src/email/service/email.service.ts | 62 +++++ apps/user-data/src/main.ts | 22 ++ .../phone-number.controller.spec.ts | 18 ++ .../phone-number/phone-number.controller.ts | 53 +++++ .../src/phone-number/phone-number.module.ts | 12 + .../service/phone-number.service.spec.ts | 18 ++ .../service/phone-number.service.ts | 62 +++++ .../prisma/error/record-not-found.error.ts | 1 + .../error/unique-constraint-failed.error.ts | 1 + .../interceptor/prisma-error.interceptor.ts | 26 ++ apps/user-data/src/prisma/prisma.module.ts | 10 + .../src/prisma/service/prisma.service.spec.ts | 18 ++ .../src/prisma/service/prisma.service.ts | 15 ++ .../src/user/service/user.service.spec.ts | 18 ++ .../src/user/service/user.service.ts | 62 +++++ .../src/user/user.controller.spec.ts | 18 ++ apps/user-data/src/user/user.controller.ts | 53 +++++ apps/user-data/src/user/user.module.ts | 12 + apps/user-data/tsconfig.app.json | 12 + apps/user-data/tsconfig.json | 31 +++ apps/user-data/tsconfig.spec.json | 14 ++ apps/user-data/webpack.config.js | 8 + docker-compose.yml | 47 ++++ package-lock.json | 51 ++++ package.json | 2 + 80 files changed, 1760 insertions(+) create mode 100644 apps/auth-e2e/.eslintrc.json create mode 100644 apps/auth-e2e/jest.config.ts create mode 100644 apps/auth-e2e/project.json create mode 100644 apps/auth-e2e/src/auth/auth.spec.ts create mode 100644 apps/auth-e2e/src/support/global-setup.ts create mode 100644 apps/auth-e2e/src/support/global-teardown.ts create mode 100644 apps/auth-e2e/src/support/test-setup.ts create mode 100644 apps/auth-e2e/tsconfig.json create mode 100644 apps/auth-e2e/tsconfig.spec.json create mode 100644 apps/auth/.eslintrc.json create mode 100644 apps/auth/jest.config.ts create mode 100644 apps/auth/project.json create mode 100644 apps/auth/src/app/app.controller.spec.ts create mode 100644 apps/auth/src/app/app.controller.ts create mode 100644 apps/auth/src/app/app.module.ts create mode 100644 apps/auth/src/app/app.service.spec.ts create mode 100644 apps/auth/src/app/app.service.ts create mode 100644 apps/auth/src/assets/.gitkeep create mode 100644 apps/auth/src/main.ts create mode 100644 apps/auth/tsconfig.app.json create mode 100644 apps/auth/tsconfig.json create mode 100644 apps/auth/tsconfig.spec.json create mode 100644 apps/auth/webpack.config.js create mode 100644 apps/frontend/src/assets/CTF.jpg create mode 100644 apps/user-data-e2e/.eslintrc.json create mode 100644 apps/user-data-e2e/jest.config.ts create mode 100644 apps/user-data-e2e/project.json create mode 100644 apps/user-data-e2e/src/support/global-setup.ts create mode 100644 apps/user-data-e2e/src/support/global-teardown.ts create mode 100644 apps/user-data-e2e/src/support/test-setup.ts create mode 100644 apps/user-data-e2e/src/user-data/user-data.spec.ts create mode 100644 apps/user-data-e2e/tsconfig.json create mode 100644 apps/user-data-e2e/tsconfig.spec.json create mode 100644 apps/user-data/.ASC_MANIFEST create mode 100644 apps/user-data/.env.example create mode 100644 apps/user-data/.eslintrc.json create mode 100644 apps/user-data/.gitignore create mode 100644 apps/user-data/Dockerfile create mode 100644 apps/user-data/LICENSE create mode 100644 apps/user-data/jest.config.ts create mode 100644 apps/user-data/prisma/schema.prisma create mode 100644 apps/user-data/project.json create mode 100644 apps/user-data/serving/.gitignore create mode 100644 apps/user-data/serving/database.env.example create mode 100644 apps/user-data/serving/docker-compose.yaml create mode 100644 apps/user-data/src/app/app.controller.spec.ts create mode 100644 apps/user-data/src/app/app.controller.ts create mode 100644 apps/user-data/src/app/app.module.ts create mode 100644 apps/user-data/src/app/service/app.service.spec.ts create mode 100644 apps/user-data/src/app/service/app.service.ts create mode 100644 apps/user-data/src/assets/.gitkeep create mode 100644 apps/user-data/src/email/email.controller.spec.ts create mode 100644 apps/user-data/src/email/email.controller.ts create mode 100644 apps/user-data/src/email/email.module.ts create mode 100644 apps/user-data/src/email/service/email.service.spec.ts create mode 100644 apps/user-data/src/email/service/email.service.ts create mode 100644 apps/user-data/src/main.ts create mode 100644 apps/user-data/src/phone-number/phone-number.controller.spec.ts create mode 100644 apps/user-data/src/phone-number/phone-number.controller.ts create mode 100644 apps/user-data/src/phone-number/phone-number.module.ts create mode 100644 apps/user-data/src/phone-number/service/phone-number.service.spec.ts create mode 100644 apps/user-data/src/phone-number/service/phone-number.service.ts create mode 100644 apps/user-data/src/prisma/error/record-not-found.error.ts create mode 100644 apps/user-data/src/prisma/error/unique-constraint-failed.error.ts create mode 100644 apps/user-data/src/prisma/interceptor/prisma-error.interceptor.ts create mode 100644 apps/user-data/src/prisma/prisma.module.ts create mode 100644 apps/user-data/src/prisma/service/prisma.service.spec.ts create mode 100644 apps/user-data/src/prisma/service/prisma.service.ts create mode 100644 apps/user-data/src/user/service/user.service.spec.ts create mode 100644 apps/user-data/src/user/service/user.service.ts create mode 100644 apps/user-data/src/user/user.controller.spec.ts create mode 100644 apps/user-data/src/user/user.controller.ts create mode 100644 apps/user-data/src/user/user.module.ts create mode 100644 apps/user-data/tsconfig.app.json create mode 100644 apps/user-data/tsconfig.json create mode 100644 apps/user-data/tsconfig.spec.json create mode 100644 apps/user-data/webpack.config.js diff --git a/apps/auth-e2e/.eslintrc.json b/apps/auth-e2e/.eslintrc.json new file mode 100644 index 0000000..8852e20 --- /dev/null +++ b/apps/auth-e2e/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/auth-e2e/jest.config.ts b/apps/auth-e2e/jest.config.ts new file mode 100644 index 0000000..750cb86 --- /dev/null +++ b/apps/auth-e2e/jest.config.ts @@ -0,0 +1,19 @@ +/* eslint-disable */ +export default { + displayName: 'auth-e2e', + preset: '../../jest.preset.js', + globalSetup: '/src/support/global-setup.ts', + globalTeardown: '/src/support/global-teardown.ts', + setupFiles: ['/src/support/test-setup.ts'], + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + }, + ], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/auth-e2e', +}; diff --git a/apps/auth-e2e/project.json b/apps/auth-e2e/project.json new file mode 100644 index 0000000..bcf1d3a --- /dev/null +++ b/apps/auth-e2e/project.json @@ -0,0 +1,22 @@ +{ + "name": "auth-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "implicitDependencies": ["auth"], + "targets": { + "e2e": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"], + "options": { + "jestConfig": "apps/auth-e2e/jest.config.ts", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/auth-e2e/**/*.{js,ts}"] + } + } + } +} diff --git a/apps/auth-e2e/src/auth/auth.spec.ts b/apps/auth-e2e/src/auth/auth.spec.ts new file mode 100644 index 0000000..e8ac2a6 --- /dev/null +++ b/apps/auth-e2e/src/auth/auth.spec.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; + +describe('GET /api', () => { + it('should return a message', async () => { + const res = await axios.get(`/api`); + + expect(res.status).toBe(200); + expect(res.data).toEqual({ message: 'Hello API' }); + }); +}); diff --git a/apps/auth-e2e/src/support/global-setup.ts b/apps/auth-e2e/src/support/global-setup.ts new file mode 100644 index 0000000..c1f5144 --- /dev/null +++ b/apps/auth-e2e/src/support/global-setup.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +var __TEARDOWN_MESSAGE__: string; + +module.exports = async function () { + // Start services that that the app needs to run (e.g. database, docker-compose, etc.). + console.log('\nSetting up...\n'); + + // Hint: Use `globalThis` to pass variables to global teardown. + globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'; +}; diff --git a/apps/auth-e2e/src/support/global-teardown.ts b/apps/auth-e2e/src/support/global-teardown.ts new file mode 100644 index 0000000..32ea345 --- /dev/null +++ b/apps/auth-e2e/src/support/global-teardown.ts @@ -0,0 +1,7 @@ +/* eslint-disable */ + +module.exports = async function () { + // Put clean up logic here (e.g. stopping services, docker-compose, etc.). + // Hint: `globalThis` is shared between setup and teardown. + console.log(globalThis.__TEARDOWN_MESSAGE__); +}; diff --git a/apps/auth-e2e/src/support/test-setup.ts b/apps/auth-e2e/src/support/test-setup.ts new file mode 100644 index 0000000..07f2870 --- /dev/null +++ b/apps/auth-e2e/src/support/test-setup.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ + +import axios from 'axios'; + +module.exports = async function () { + // Configure axios for tests to use. + const host = process.env.HOST ?? 'localhost'; + const port = process.env.PORT ?? '3000'; + axios.defaults.baseURL = `http://${host}:${port}`; +}; diff --git a/apps/auth-e2e/tsconfig.json b/apps/auth-e2e/tsconfig.json new file mode 100644 index 0000000..ed633e1 --- /dev/null +++ b/apps/auth-e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/apps/auth-e2e/tsconfig.spec.json b/apps/auth-e2e/tsconfig.spec.json new file mode 100644 index 0000000..d7f9cf2 --- /dev/null +++ b/apps/auth-e2e/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.ts"] +} diff --git a/apps/auth/.eslintrc.json b/apps/auth/.eslintrc.json new file mode 100644 index 0000000..9d9c0db --- /dev/null +++ b/apps/auth/.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/apps/auth/jest.config.ts b/apps/auth/jest.config.ts new file mode 100644 index 0000000..b4b7e15 --- /dev/null +++ b/apps/auth/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'auth', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/apps/auth', +}; diff --git a/apps/auth/project.json b/apps/auth/project.json new file mode 100644 index 0000000..76da07a --- /dev/null +++ b/apps/auth/project.json @@ -0,0 +1,64 @@ +{ + "name": "auth", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/auth/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@nx/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "target": "node", + "compiler": "tsc", + "outputPath": "dist/apps/auth", + "main": "apps/auth/src/main.ts", + "tsConfig": "apps/auth/tsconfig.app.json", + "assets": ["apps/auth/src/assets"], + "isolatedConfig": true, + "webpackConfig": "apps/auth/webpack.config.js" + }, + "configurations": { + "development": {}, + "production": {} + } + }, + "serve": { + "executor": "@nx/js:node", + "defaultConfiguration": "development", + "options": { + "buildTarget": "auth:build" + }, + "configurations": { + "development": { + "buildTarget": "auth:build:development" + }, + "production": { + "buildTarget": "auth:build:production" + } + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/auth/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/auth/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + }, + "tags": [] +} diff --git a/apps/auth/src/app/app.controller.spec.ts b/apps/auth/src/app/app.controller.spec.ts new file mode 100644 index 0000000..de8007e --- /dev/null +++ b/apps/auth/src/app/app.controller.spec.ts @@ -0,0 +1,22 @@ +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/auth/src/app/app.controller.ts b/apps/auth/src/app/app.controller.ts new file mode 100644 index 0000000..dff210a --- /dev/null +++ b/apps/auth/src/app/app.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; + +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getData() { + return this.appService.getData(); + } +} diff --git a/apps/auth/src/app/app.module.ts b/apps/auth/src/app/app.module.ts new file mode 100644 index 0000000..6a9bc16 --- /dev/null +++ b/apps/auth/src/app/app.module.ts @@ -0,0 +1,11 @@ +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/auth/src/app/app.service.spec.ts b/apps/auth/src/app/app.service.spec.ts new file mode 100644 index 0000000..42cf0a2 --- /dev/null +++ b/apps/auth/src/app/app.service.spec.ts @@ -0,0 +1,21 @@ +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/auth/src/app/app.service.ts b/apps/auth/src/app/app.service.ts new file mode 100644 index 0000000..cd8cede --- /dev/null +++ b/apps/auth/src/app/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getData(): { message: string } { + return { message: 'Hello API' }; + } +} diff --git a/apps/auth/src/assets/.gitkeep b/apps/auth/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts new file mode 100644 index 0000000..a124382 --- /dev/null +++ b/apps/auth/src/main.ts @@ -0,0 +1,22 @@ +/** + * This is not a production server yet! + * This is only a minimal backend to get started. + */ + +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; + +import { AppModule } from './app/app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const globalPrefix = 'api'; + app.setGlobalPrefix(globalPrefix); + const port = process.env.PORT || 3000; + await app.listen(port); + Logger.log( + `πŸš€ Application is running on: http://localhost:${port}/${globalPrefix}` + ); +} + +bootstrap(); diff --git a/apps/auth/tsconfig.app.json b/apps/auth/tsconfig.app.json new file mode 100644 index 0000000..a2ce765 --- /dev/null +++ b/apps/auth/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["node"], + "emitDecoratorMetadata": true, + "target": "es2021" + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/apps/auth/tsconfig.json b/apps/auth/tsconfig.json new file mode 100644 index 0000000..c1e2dd4 --- /dev/null +++ b/apps/auth/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/apps/auth/tsconfig.spec.json b/apps/auth/tsconfig.spec.json new file mode 100644 index 0000000..9b2a121 --- /dev/null +++ b/apps/auth/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/apps/auth/webpack.config.js b/apps/auth/webpack.config.js new file mode 100644 index 0000000..81db92b --- /dev/null +++ b/apps/auth/webpack.config.js @@ -0,0 +1,8 @@ +const { composePlugins, withNx } = require('@nx/webpack'); + +// Nx plugins for webpack. +module.exports = composePlugins(withNx(), (config) => { + // Update the webpack config as needed here. + // e.g. `config.plugins.push(new MyPlugin())` + return config; +}); diff --git a/apps/frontend/src/assets/CTF.jpg b/apps/frontend/src/assets/CTF.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0801447d53f7f8e7c28df420f8743c76bb4efcde GIT binary patch literal 119264 zcmbTe2UJr}*EbqPx)A9ILR1h`M3gRIVg(T~A}R_>{)#jaA_5X10^0?)?1gQW+9Dve zgU;_X3IduO&Jy5s<8lQjwG%7)8NKkjd zPzSJWQi9TZj-C{fvAHe0*IQQiNy2*(wNrW3a+f<8>Uwt`hKP#ED=6+%+NYtp|G+_g z14AR@V zb=xZ<)QiPef!-CA`n8 z7FE-`%#geDutQ8&f9o+uEt<<^^XJu{m|MzB{|8ujh|Ndy_S=q~aV!^s; zCS3=%SMyZKp?6~{tC1HN|e>V_2-QL-$8u3^uuSshI$L4BH z0-#R#WYr5kY#WyQ?rt)}A&awH`i#2kP)n=yUMFtl5K3vY#WFIu{TUy&b<7(>;=>qz zhMzyXlNe{n(M7C>VOCtb+V-1IdXm^)O?+7DzF^1GCl9K&kJX)D$=iU0+UA#+*2NQD z8!J7$<5Sw7D~UR+zJ-@L=55BE5v;Il06 z{_74omwv>^?vGmjpmUGsOV|z~(Sw>iNen1H_HyAuZ`!twi0sczJNPgn_WO;|dr}o? zq^iUb)R;S_qZC#C3)i4Efm0Uyc2hGfgm)3k5g+BEDNKu8S)G^*7-1D z4MHEHuz3D9G?~;AFoV4Z7jaAc3WfP1B906ucD6c>n|FS_T}sv8w=!|T#o_vy{^sE9 ziGKDjLg?{=5$7inLHiz13v_l)9n{j z5KX@T!_zSfdKcmi5eALJ)#o>-e;`B%a-2Oyyx$;}QGB}nj&J|M$JuW+%Y0bXj1h&i zJ$0j;We$2U5j@dCwhP`IQi4nP`#q_Pqn+k`RXaWO_QbI7wDKOzWU(1rCxwSoduh7X z!OkAqbwwZkcpaO?#Cx;UQ19QM;ZN(%U!hVYhbgpMMMFMptJ?jHDV>N<^#U-m@lQ;B=w1yaq=mCF z-QiJ98GDHlc|EQ#Htz=vy`kr}9ry(!D->(As;}nI zeGi(qu_&ZgOY6>j*vC4~P~)2_+y!%ClXPbNGKLS+h~y0e{NB~J?KrXpGb_TEqKxx0 zPeJ#t_WOy8sw?}~>U-=Z#1}p?-ko-APf56X+&8jvT=TfW3Wg(nn*NKT0Nf^uH3crN zr|l{)?>PMN5o9TJ1D=idpVmJB+I21(-gOnC#OCKz1t9$#M5^lAR?%v|Zoi)B$KN%~ z7HL_se8Y!vlwz22NMS-dqJYGa(ZlqJ6-Sz#MZ}nCduVR9Jk>3-OCouOLSyQrc&VSc zaQL{#Mr$4z58Ot9I-2tv?5pNPER7oV%WMyQHf%`#QAtCZBeavZAs>-Yl!5F`U6*US zznQsEavtt#KGt9vSx+4gVe|UvfT(vGnj2bw{g)DE`y!=gjmSHH2T=}oebJbRe(pzp zmOQ_b{_PiH)rj4^qzfCQJ@$Sd5ZeiJ+1TDK?xMq4UPDCA?$o=rh=n0FK5Tw%R`ca^9fKB* zbmzx*q!?&ao8&c;1sYiu1&?Eg3&K$uzCVpTg`;^Uzda6_d)gNuho^uKrB@NzlfDOl zLwl=c-UkT13H5y}Gi+m&Ee+H1c= z6wJMeW-0+~R_)fdq+L;qdpcE+Np_3(cFG_B#o44&l6Cs?d18>A74(5NWbhnC4sXiV zBvkhn=Z^DX2=$FxvLCQ$6sVv)u^TF3HTf=6#50BS{YR1BCz?J=sv zY?Wxs zCeQN1;lq5G(_+xIYsG9x`f6O&A*0t!J)T$>J=<7AF|E0gel1`6$VfTY`qMc~n#c4( zCT9C(kokZQ`?0ivaF1Y2$1&|s3)lo^Ah-ueUEf^UnR@lN?-4W^SF670LtRSG7Ohto zWfY`*`p%)wyBcsR{~#D=FwuONl#~nD!zTRMtoy#JMPe$+hoH<8HejH@h07SwDEmi$ z6Zcagf}<|Ohb3v%=hESVVwP4Et>U5%Lo_rX#i>ppZg+%Y6=BGrN9b;(G ziaD}mnp}ZX9UmrucNUv7vUxAK8Z&);#qe0dWu|ABt96R4O9>*Jx`U(5^r!ufVH#Bz z#~@$OB@C6>7r3kNwq!nR;r#}j?T!&4v?4^1`+0RF#w9=$s5a06+^k*es3L${s|4pL zJ#I_#x%;V9JH6}Lodd5vM(Jp0oy)ZBo8CD3Hhyd!U|7PXaVs>Jbfytcbb;w?Mv=j* zF^^9i*b>~Fm(l&~)(77W=?OI=rXUGpO4qm=_8aGz?EJ((_W0zqD8ZChjHK3V??Tk# z?Vi$x>mQau106oMIy%_69K{ON?o84{RN)33PqGi-&5O9uoQ{oABlxf#Yx44=2=4oe zJw9VxNBXt>p_l6Rj=*Ls+B`C|xD|y#Te&%3vecT{ZPB0;Pi&ff0l&T73_(+IrMWf) zYha>Da5L}QUqMgbbXHLz?&J3A%*dl>W)Gg2+?mf#d($_arqJ6!13w&oYt*DZZmK!N zhb5w}GJ^!eei>_2n&ihiD_@jYCt{71vjg6eYvAF5Dg>kAmW(VO+RGg?2-eJ~AjGk2V$`<+2&r@dIBFl-^P zrZ!JuTwBnhSu7A4q;ZM18dMBVfw54K>hg6HJ2;m1n?~qj9g6@*ly+vMIIVIgnweD$ z+d4d)4--nnq3G5Vj=Q;)l(7#a*(#0EGgsdgw9{iQgbe!=gF7ny(d%YkxE9f7M(K4o z`ug5G4EXG8TO4Jv%MdXSFtJwQpv%r+ylJ+v<)@%#tt7xG>pj1H1MznXE%4&t`WQ)s zBL)hOn-UfAmfx8#D#{EfVcIU`*ip5t7u(zUu$)&J%Z7Wg`1nRSt~tk~+wAHW4zk^V zq%_;mXkXf~kkK>0g%7iR*rAz9Vkl4@ce3SyYE58_1TfGgiQ_B>?yFT5__gI`D|+^1 z5ZCb*ABIzI`LMDVv8eV1$irCZhu>K-BFNG2mYOR>K79GlOw;n|4PrX|>%KZB!S7Ho z&RqIMO8I~%Vd|it!TwKupFis!_ZXinH+K4Fb5=Iyzx@O1uT%P%PO-m9w86Ye`!R}E zEj<4ASvlUSIpahnpw!tZ(~`fU&NErl%`rP_Ou|j&BJG{V3kuH;c4P;-M&B+9dlTaf zTmM0xPU1=K?y0!NhI~QwBLafg7`Rl2-7$mQ(S?_AiO`1IxJ9MXyp}XdV}nN)ie%M8 zc^|I=V!Q@SVPo~HJHS8+Pi%xqsV;gju5_!H?DJ``n48DGa)eYb5xBEaiZON4QTAxp zrLwYhhvgZP(+n8&7?%~TyWylei0_CkCG7u7ZS{&nbQrPx$hfoDN;^Iz6r0z%P=r2! zpCQ~U1n!+Fu5AOg4o7`;5VoBF%W0#j5my|k%t=#C&X&ervI9_k85o%@Sn)yP8yI3B4sGLt=Nl=C$`6t_9l(sGdMbD`O*2cg@>Vv8zwl zaS9jm`$@y7L782_2%V*Ksbmg9fd0|sE0NaFwfpvy`<_VEpeO52;@db|3y(oT8J+S@C6NycTFs(SDmSmG*1|b4izi$4 zHZ~9pXu?2~p!$n`!FD{c71h^T|0tqp@67BZh1ZK5Zpb3+&jDjYD9~8hUf70-=E!fc zUmlkTw51nEe>E~*6_ zN!3Q16~B|0zMOhJ?^0~xe{OGgPMWUJ_)5kH*WG=qM=>#wh>t8dGNk%Z6VRPT0uNfu zPo-Rg-ki8itT(d9G(Fff^V#TKpRs+*^4BB))3J*abfxA(#XI}UeCDn=H@EfgFb_aD zx|=(6RzVgp$^L6YVMc5*1G1Likddj0D7#KZ)Ryu zymqll@9X*Wpr8Dhd@56wtlnlWGi`_B|DS<5gx^trarytm68&$1%3qqMJ@C@DbG3Vl zr#rJEn$6KitFC_9HE){tbIcBcP`iwn6jluPGiB@(0rH(8S3tG#hCB#f5Xaos5ORZV zU*`-=Q{tB=tGXu3lwaasdiLz~$-C9E;HA~uGD8bENI!}Z)Fwvuj!XLi!WNMwLGwm`Cw5({*i|~+VfzdmUb){aw*$^ zE0d#JoQ(scuUFih3p{H9K`+HNsVi=|XKw5|g}fEo8HOyFzH`x%;~o%^C(M^{Mad1bUf z#d{TdA~I|or8CKZJ|GT;#y8wHJ5zt>LD$5agNN;A62N_o7@mkSq-zjM84}W_N``)w z<9?j4g5wU3PbK|GQ*^_U?-CF4Y-{h+2ZS$}by@FK{fzlY`bOdG@ZpJOK!#(Jtq@fu zgBOadL+TWYbb!;14>dl2(Rxyspm0(mpFLEs68uEto4CvV)Wln!Pd8`{aF?%xiWTVs z);6^nB7E2*)CB$!>PYKK;Nn-^k8>^Tg~U0NG}1@+{JFrej-jEUhUGT>PR}c6=Xy5u z=p@ioYwTif_Px!N`E|Ak=QfiM3oB^Z$lc8hPkt*=m^(m3nd}+;8Glr>?Yngt_RMi4 zXNL)UH8~+)gVd2mYBO2rYahucnN$h}7gY^I=MOJ#|-^R>>%*AS`^nxOlLG4_j6V z7G?zTBuyYGO*wwa5?bfE-&X-iM=RC4-?~yG))&T!+Lp#!EA(QIT;d7oC_iQ}6~|A0p6>pFY{A{@k9#I;!pi zOx$BZc*SS|0^x!t;9sMTWkAf;I32N&@&)h3xL8grZ-EBVO8==|0)czl_f(v#!z-0V zY0ugFFIQ9$3#a4Q>gbl`H*IQdBn^y1Du7TUa?Ajk)rH0r*SMMYD5*zS%jm!d-v$B!zbF8=hesFxh_QvJY zh(Q%6;?nq1-%o1s5=TLkCkk1bZG+6aBr!hB4IWh%@p&A#aBU7PfHSr7STNr|g0EW% zIOl1_iI39%IjMqJR9ZdqQmlVw#B^8Z7y5j{S~pI!c-r!9Rx}3-=0fXlLuemti!gvo z5gMtC^C-pPnWAexw}6^kOo2-{?e`Jtqf7fLbM_fu#FdiUF`znep&D}a5L6(g33InUq9(HfXN<|OXft>p6K~z##uWfsl@C== zc0e+i;as0LxIAOB9nfT7%TFhS^>v>wMj2Z9O;w$T)_Wc-u2qFWru_weAAM=(9Bb1n z#fi2AZwdM|+Vp+D%=`*|vhq0BVgu4z8*uQ;x%L^jVjx5B=*5KItJ`RC_`get^jbtR~}U30TZ zHBwIK@b~(-uX}zs@xI;o;jr}FS8RG>a^o)ufm|aIb@4LXX#7cMr=ZC>f=+_ImH}d= z!O;ud)nq~B4QUhdf$D95^E&rAL;*kuPKcm1C*PoAdzDd!OL34s+^AskF^(YBxIS+A zA(YqON6T8U8S`iZ51=^mnq!R|8A@$Uz-o+&;~=#zTlVGhy9X(oAAbg4u0gOD5816D zw#l>ZB^N!&@&4j@<;-9AuoK;7jhI~_gr#0N1HKiS%xlh zM3Cf?rmr(=Ncc0Ymd z>zrK*&}MkS&8aiNJOS=w;BHEF(XZM$c*Mz4$?2`KK_kz8@!Ff$ScemGXGeE;d9DTT z<%ySjyu5tf%kwT(_7f*Nn^;viPS?d7I4`m@J8~~WY0&^LWhKxj_}wZx%# z#EhrtjuPPs%q-E#M2hSruL*P>`=g95Xioe^hcAeJKm<#ll2)Wl7Em<|)rU|fwYR_A z`NCFAdoy<`)y#<}4tYJnLtqabh*GlSNR{(MQ}k0QjX=~sZL=G>?zSc*VfUoN>)d6a zv7&9uiYj*FQBe!=EzNh6%@aKUO-%u)2i-HcFLRLM9MkItXxV#~5>z6`Sv#{O?jgjy z@s45Pzn|4|MEi72%QyTvJD`Rz)Yx0f_BBjAra&kUf^wJzK_&$C3XuD7Wt)tVmizkv zQ?}tw(h~g0QYv->Yjo^j!hJRv&92Z;N|TNbgPZtF-FH5WOGv z_sT)bgXmhG9-}vdhLW3kR2HA|rN7nr%k|OfsahXT-TSGU=y8kF-*P=WQ6ul*Q*AVj zLi!SDD8Lhs`HWX&zE~I}{H7}I3;o%BcnI(9)^&mYn>q3O!r=Y3Lwc^$IDDTJZ}T88 z>d@CQN^$h&Z`@?aUAqVz=3$v9IUX3RLNKg6)p%kF>>YCi8AjR*EFUJWQ!Qaw$e?fZ zN%kEKU62VJ3*f`^$v;~>{#gTt-x8Vlt~zF_v` z4tWMt2xRRmW@EWeyrx&-8+Pxb;!3QPbr#WlnC0piIQ0snvzrsFD^6Q2@zKQSQA)kkJNS-VbR z+<*5NKKy{I+0MFS@{$J3g3{Xr29-CmJu==H6UDq->s%5aknyYUPC3;Qhj z9E%LVgp_xJbQ5|whajI3dw=#l=wr+FK{;bZF%ZR(&~CSS^^0!f-39Kv(+c@HZLOuc zAG^7R;Ht9^A5SS9e}m-)r;Ppja4|sNCLS$o*XCy?v;3MTxe8VSV@se$UnBwUW_P0u zBr=gT&2#;R7kaO?x8guPtoWs;Rx~3e+_Opr)U*JBtWcJxj74GBnA6CfFmuYLM368? zf%pSpTm*&^m#V4XilHzA+>ebUh;c5r7d~eygfFh?uUy`jtPttku(2oHY*fYQ7m-~s zP)o1c1)be-MkedUsjY zysXk}Vyr$`J9g~Y+l*i=mvD%yRjX>!Y^+hWZ`}Rj#p1(xmP5(h!Np*8+Le$5FVxbV zw9D>x2VV>KR-I*17UUqq8qa;TcsHM$jT%c()h>(ZzvJmpWA8aWsF_z*Xw`z{1eO9& zYPQ-BaJjsc#kmODjFaJWQg4gp9}J!Jn?SwSQJ3Ju;-fhbRY*~wIpb&>5G>yh;ZRBf zSK=|#>G8_8+?Y$)kK_BmLTnv7LN2<~qP+BAl=Nc2F3u+{0iMJI5Nouc$-cqsL@=&+ zdUevRtVS-Qgm8PO#-|S4KjUw7G=aMsqJ3b?sz3P|${h=8k>+2~$pfMFn|9SpPg7E= z_V=ttbmizfw2I9Qo|PeK+`x}S?D?*cz~jR%X|a-cBI4|WkW>h09Q%1v;8s4&+RdOf z=rnRaQzZF#VsQ3lpfJFzGELD@5m<;Lq|mV2>EBb0V;yy4!Yv1p>sZhPK?JGw1COv) zqK{jcQrY6AZOM=~cx3XmFF7p|a?Y%)Clz6=9FIk;V=wKhNFJ9!*CBSK*!QU*cj0u-2jyBx&!y-{y`bLRu_y>{JG>lksP<^p z+V*s3;lf^a?I0^H#n2@DW3PX2=fnI{Gd5k!(@;!e7tb*Tud+pXwRVP8s*ElWRTDUx z4zbVcgRIZW_foci^c$~RO$56BP(QH%#rCREJ+x zIg2X+Hj3{_I~KivHx}GgL{;1l#S{X0#qr34_>0^`q$1uLT+14|4tCX{;Hz;~^+iP; z8#bpuS-WoU%KSyrkv8SLGA7qh2OVfoNU=a>KO(e2`63UI@ZTwPiY$~j4OT@=bRJr7 z(N$0}s+O3(iC=;na3wHi;&-%bwq{M7ttT|rmL{w1d}eVZ+gyKRaRXn`HuV+xbD5w@ zKj8dwXR~AL>9or{SE#OT{6p8bk+YBg$$$+j$ z^;r!5&oc+ZQBuGP=G$jK-l|u40X9j?%$k6f%Spo-hy z^BJ@})a7VBjTsHNq_-g`zyCJ6^m*w^Cz#SI6+EQC*>#v8f@@x3LDGh}x))J*@Qyj2 z?=^`pbly?k#W`TuRcXPv<)|S#!22ML3NCm0f%oT$LCY*S3^uXv;;p-ZvA2-%hH7)9 zj{mZ*>S_d(3(DVqs}*Jc$sBpl{iQZ-S8+2xA?7|Oo`aZoX2bCaZcK#?H)`y)Vj+A7 z^@LC*J^LjDEe;PITOMyaa+)+|%i9Rt7(GDe6o)?;jv38n?csXLEPx8N6+`hyDfT(T z57yVG*0F5L=pxIpY!`e!8-lqjNMRV3~4-^!p1!CKclbC(vT`7j6f0I^mGw*Npxr~GnjOaA;UcveaO zDqI0`p6AwD5~JVd4cOPWe%4MwTlu2CeZQ#MY#gK$FD_ttN}Rcp;y;J4X5S!GBN*uH zwn%$wJt7*4;>H*wUt=;W)=em3UW6aa)&7JUQmMaxjmE^>*`O@W&SHihD~i!pi-|@R z$bY75+c(nS(S;%qu`Yn~xb!N+s%6L>v!K9q6;-sZ8F(wR-bTq9$ip1s^>1S9h~WO> zg>=R`1oqjeIpWTqwWnmlo_o*CZ|H*w+ZLDlGb)QV9=T4x7)MvZ$TPL9Kv0FT&bke% z(a;0aH2H`!{|G$dgTt)7!57lo-Qkb0J{+(9(WRb|kv0v=2aaitOSkU6r;T=RMlgW~ zfjcVji8jUjLrF4q6x)_hrzBGvjq8KDg7tZr-_sCnz1X9%oYSmIydlxlBegmyhUi;ZKl=csN#-MXmsqX2G=CC^_1Ps;+ zVSht5018C@Tf(h|n52n6m^Og3OPH;Rw}(1D2U)c<_`jrV5jR$!oIkU<6fKS3HQO8Y zeN^Ih()u66>gAz(mo}=~IINY6r!D1xQ`a{Du@&@SG3zb})vplR91n3{5{%s}KjT~* z0!{nx?brbHHBD(HF=p!Rzw&+&IdCPV7@xy8wmf0)A#{_(QJy*j!C9mv>aSXtI(RxVeuq<~*iGAb0xNz7%i5g0eO5PGbP5681zrZrHXl9VgMiq~0Yqn#>W3F>rqqtV_l=YEPSF%1`oa5 zaJkm_>C5u$`X%y`OxC)V*ek+eV!gn*YhiUCUY+$W&-D29zw*MT+qr)$-ZZ*UJUTqcIHiQy0$;28wSoL0N* zGoj$(STRk$fVEo!OOvR`YNn9_J(7clh68}a=+*4gq%iMqyh#jKeGNQ+^Nn*#3ooq` zatIys&x&8mFCVqGQF=SyJ)rW<1~Gec391Q-LH&&guX22tF^$Pel4+$!TY?M+G=EI) ziN&v=*dn6j$yCLg7Y}~ie<{+8LtL_7`$Tg;pL=6}y*y+h(rmw%xfU1f#LqjPrp{6zYM(0$?@r! z+?Oc+Phk4+g?D_|!^-Ju4CDU#!VPHZYr8o2pf(i3uzl+6;I+5N3{-7f4s)bMrYR-f{(>0yjqftZh%K!LRHpW%e9@j)bBIj6MNXmuC2&2pxD`b1gc}aUR7m z;=_tEAC1D9#i7VOvzByHBu}TyuT|GFO{du1uE|?|mEBaSbw+2?%2c&78%jUy9^QOT z%3Cb=57sltW41QiXRFH97ecVQBb{70dGg3MyIOBU+wC#P>?dKR?-9IiE+RrNk~DE0 zUtOCtSa13WDkxm5;DnLvm*GQf+a7&Xe;?gEgwn zNt;$kXdj09^I`v>`aK#jjPvjiC@;kcVDI4^Wg=n=Z<*SbcH?i+;alAM0^iil6@PlY zs2=%iL+YCP^x|H-S=Vqg1sXzN);%56t2KELp>jc--q{xJTDc1>bJK7 z{Q{Txu&%L&mN3R37#Vys#S1tqcbC>r5ZHM<*Q@|-;%#V$N`sibx!ddV{ z_%QaVxregc^;)YTsVTz_UZVUEkb2Ognz4du>IG8z}7tW9AyZ^FMQ=kCeKY-(oy!L(Mw=eOb4AfxuX>mubZ zai8%EM`VltD7I}W=h-hY<{@!SpKREz9>|%bUsW->+qc4mo(?J~XQDVN;^T2QW-QrUNBUm)oH`o(je_Kejg%+r`Iqw z7ygo*j^5V+DSQwO2&2ON_^@c?vF^l{(~MrCPqv2oZ;Y8>2^)4f|5--@g5cVl}u&j$_`^+z*0JPpnR>zxQE| zvo3*t89?GQ}*g-OH>na0-HRo?I*tIA) z-Y%0``87=X zGfeYmI!nI;iK*i?&{TzKfp65pvYys0U{wIdi-v2&+UKix;OB)sP~Z7ogPDST4YDBni&O(3S>0Si&HRNih$9 zRQW4aUB?gAq3sn7=LnJS{c~y3iR*qxH}8j2kt+ltpXE7Y;Y>WD67eQH*oMp>fvtc9b(>@DEQUa*PCRG}*$>7e-YSkkWY`D%a>D zlP?>OvbD3*ODy^mYju%k-#7ZT)>RdE7e#4*h;zD%*t-}18S|L>-9LPZ54*nv>ZH_A z#Blma?|HxNt(TsfdFP%Y9lTYxExI2ytob|WN~7>ZDQ}X%ZZc8Z*>?Jk-Kx;lxa^mc zzIiWhHyy|x>cdvv1NJk`uZI3C38}(_WiL3Lzga=`<4wPJuq)l`TpWMFyX;-jw~`WK zE8<`;5L;n8#)m~#$#t6rGZ78;UQr)si>Pkhg`O@O+S-R`i!0WKQZn34PA&HvG?#x! z12QyXG`a@ekEU5XC7{6iDV$BgmVL?b7|pO(v_y|YII1+#DL%>nt3k@4s{r;Xh}M0b zt2sZLT?CcTb0?GzNWJ;xZ>v3)+{?t*S3}xdhZO>R0?#Yqf<1*_x0--VYSRCPpNQ<-~oz8fS5989zi6 zn)}%Q$FLt_V`q}K1xV@Y9dhX6mN5@F_bYuhc(8y^1rqwAYN3n+>w2n}G;h=OCxYQ? zv5?2UPxy)0Mo^f67JR8LsBH_*fi7A3$qgzO#RHlBdKkCq=Hj4p|GYL`i2cl(9)0b? zhn?L}(ORKcfKa5oZJ8Tl@RDFa=mwOB8hFRMU94D4;ipR)-G{$sdJ(!*Qg@QJTgY6z z>6$)8!2@IO`LH}hEfi#s=i}&n*nkebuuK}lQte;mbm@s=#Q!{Fkt_E@H*b7p4M(HnNt$r%ha z)-?!FIFb98SP5Vh_H)ZPIS!CFvqaUk?ZTzkN$ZPdx(&X0lqr%#sI!QeQV2jy>zY%I zPd9c!k?lG0Pk47|`k3uzN97u7vmEbjUa9ZnIy8A7f(*V71_H&^C8+ON^M!24##Ha* zn2wt1ZPR}S<@#L#3dAcKh~GEss|Nj3+JCqlE0TP> zAo?kDy_kZnyv{|}$NrIuZC>8h{9$UESnyn-ww0a9`%~cj_j>@zz1+_zF9Z=1%y!|F zA?aj(33?01wAA?!QS7Np`6~h9vyBG>s;KF?+kZ&?L**2cvr^nqJA-D6!fr?EF(nGT8|!DxIH>ay=OsuHN5nQhs;Wyb&8t|e)>AEx2jm>n*Pjb zfpEupX^b~W&{M$Z|I%{G)1hV~Z{Lo$M6?hgt6ONkNmMgt=U7?t`a?|SDd~q?FV%C;F5w5%B7-9E|RyY3J|z_~d?> zi>TgtVxXi0E0g<)5Az^_#=;Hn4T6I0XsG$T%zH?$P+Iy63KR3Z(X;+{!+`#41`X&w z$x-!9$fs1|kAQY#m{3*0E}k;yD5L&sT)O+yYlF=L$n~6fsBWUuVsUAulHk$cxO)sF zka~_(@zfZRqU{9u zjD}*gP>;beo=4m%>{2t8q48tp1$ANU?%KyVC(N@fV|wVGl*x-Bl-EaJ4x1ZEnO)St zuOx7>VKukEDdK1hhE3SjLqz#l?)fKIN2}r`%JpE_}A@ z=4k1_HrHMAu0wACSm3HW#zgR8fNjJ&IT~Zu3!4P7|4^nZ`1AXc!Eo9oa0s46{H;zmuhyP z)*vqiByCp+I9|2jJVPu8#=6`3mFudufkqH`J8{ny>fzPG;^?&QK4PgQFUc6RLPgg!f(Vbz|RYx zw89AcbpFS2Oz1SwK-Hv>Q}%KaE1`nv`Zo;rzDcO1h^;yNMEyl5j=Q6QI7h86O`L6` z5(j1C*4w4IIddDRvQ;#l`ol5;2+%{{c^xE`3Y#yW15Y%7y?xHpgPAmb^<&#-TDP`_MPm*`lTEVWYrQ!|j^j^CzM4s@xE3{*`a#PJ9|IFFiL1=sjdNtlnd1y{@ z>%khYkQZ+pt#T(F=EH71m6DRKO5gY6p-v9^k#}!4;JZo|#_82i;T38bIE5dh$_1Ui z!O9mS8qGWB-!0$CFN!ieEw(qf?#zZ3AzEuz@&S~b6@Y5Sp~#;}0{2rIP;wB7`%R&N z5}&s1ph24mN2{;!mb}H~Yo;ZKG!hm55!rX`M9zbgKaACKVT9|DZ92y@;SEAS1{jj5 zhmK7^2_Ow>96X#Li9hyo0PhRFThSeuGdeq<+Tv@0XJo4<Iz4nQDKPMNv;TDYF^)`v|`g;VLo_Q@Ue2T;-N4X5m zhcF#EpDpo@Q@fTmhUUm2SZ)vy4++Na(d%EONlGl$W&PnL@ zZfb?SZn>)I+xN`jq_j#VnkuK+{14#`2tm7TmfUA%!R)QPHsJg8LL0-#f;sh>;Z*Z& zUv^WF-}Xm?&)fG(gY{X)_p5YHG;af>ryVrgi*AoJO>SL6DNc}4{0OJ7#I^@xEg8SMG zjtHxOkCBDyp&Zjq&~cF7W$iJd(GV21`^Jlk%mBF-xyjhvB+OTyG{ZHNBk;bSM%;?C zweIlkM|HbzTE{+Wx8TitKTc=JZpk<+3!PW){5MG@u%gV9gffI7XRhR*<{^?8H3Iu$J2kovedQoiL^?>ePz+PqbCqO zDvEL&w=2F2W80kV;Y|`W+V!qqjQ}vNgB?ugiLF6;@!lDtp0hwL7W6Q7UF*emNzsK&2|SaV z>n}G9nnGvIg_*k3Y`sQ;8MCc6s7d|G5!|0@-NY;&cW=~vj3;Q^spY)xw;FN(Z zglL@(S;3fm@d&ycqpgtO1%_Z0NyOs(Wp+?MDqeK&W+lES+Yqn-*;`uVHzO)A^(peQWB5;mrdlRe@hahxnr~ea;O5>*fS)XHOw~raDqd78NkS?;J@2WoM zyRnK1;pvulnE7Ow|Dj_e9JLGCLX{tzcJ|Z8erudH!$t;bm@gBT?c(B^-RKHNwQ9Jl z6WyCu{)(;JlIq=W&U&}7ed%O9FSLZ29C`tJdV|BI?OkB9R8 z;)j*Wmh4KFsce;`#Zs1;N>P?1l@L==vQGBFm??#{P{ zv5cFUxqGg@&-42IUe6!NALgFxzLs;H^FHtMK7CJufA-4_O@OhsrUo>EWMtbSnYmyW zY-%cO(Zu|-5wH>O9(9IZe#I<~J}jms|Cc8nKlAh3%U{zO&!))dM zQr0blV=CIY_9;EBR?ysMi%tCmI$c_L=q-|nFuw4 zdu%_vX;p@d3x;u&wN-c42w_D%_~-7RHXj7yj5p#1jbUi7UIzF)_MURYHHz%~>0H7> z`X|C-A`4;m6s-YvnOxaZa1dtM!7KT-0$|;3O|G^#8#I_fg>v`$;p2 zwfdMSjSUTz_&@jr>wk7-o+fH#%8f>faGB1e9KbS!PL-)_}q%A-!N>9@^oOX?%3@bR79{BD8>z0T=A4wlqcwGTcG z<^d0*I8MFC5A^%Ss!hy%^vIh}s@#6t$+^F*6RTuwkvK3OeqU{4_L@wc-dVnv{N!|C zdEcTLS7<+{U;)g8y>k<~WciuaJ`LPC2I@ual}TY^dQe=}^zup~&GpcsK;p{97b)I> z{u%YX*YE|XydsQ$!Ye~!lRsf;Fvk}NPK5m&1XD;w z+O+QIY{%!>T{pvhjaMimFvB zZbh$_S2N8&QUJbG+)K$sgAFZIzmId5(LA;bs$ndgXBtI#*A!`D#O6-WY&&6&m0~8j zojlUbss{`00g_7uDbopbIO#uUaI;5~q>hnMJ^HfaAa9f`YUeQh+%&CQnf`9GRsTcn zTo&%(ekc`|d7N{{$xH0{yX0YuUmU_Wt`6T+!2aGwuB~Ml00DIrXV~xQkOBxIcMaeh zfDOoz<-0Fxh3-+e5*Novg(O%{ zph{rPzzl>oG_wf6suJ72V4iK90=JDIt?AHZPc0!&qvNob){yLJR`O+^h|3`%iY!lV zEC3tT1?SDb-kHs^>cBBy8Jtj+RB<;e>-Nxek9u6y2 z5dFHk31c8Is7r%)xs~iXa9SeKpkY?Ct1qGMwQ3z+gyfY$XugT4NYCV?8NvcwttL64 zrng5-ejYU?75pxsb#n@y*F5!8JL2JYMlpkWV_2(pq5ep6PjVgJi_ma~;7l3T8((vf zAA|NMC^MfCZ?w z1C){nnTSB9>{tC*1GU7UI!hY?4m__1T9&Vl*1Ig|92 z15@1Uj~**mOH2ak=p%0&?;UFt6q(TdcwudcaJpWAn~UEAiqR-87F0!+5jR_ti@B6` z9_snzij*lS>!li1{Z|#j>Ln`*vhv+dVdfJKuDAUO;Osx1aiSMBPC|erVqn4@av zG4m`3aB9H?w)$v~l4P?!QGJu9J!zSk^iydYs}1CSd(#QyCbwlN(hGnvij=7lMU6dQ z7F8k>CG6iTE?<=J7!yO!F4zW#|a33MAQUk@U4t}$w(l#@E zRpMB)U4lqg2W@QWRBx|oN9jz^=G$B4KLNa%a_ zXO){Z`)LU`4EFL#Ft;%r;P{5xl(D&^ilaV)HiW#ms?HjQ3T2BvXR49gH8r& zXe1OdbIB#E{!qir8BVUR_g4Ma{Dm!o|6iVOAv!xArtGn5mMoSjLQs3zbnZe(KH&(x zqWI^CZCl0SEx3Pgmzz+8T&!)2NThs}+Fnp7&lmsv64du_29C-EvNzf%e`1 z&&n!KJrynkM49#aGHtVvM{`DJ8^{wis+sWXw_q}3#$>r9q#!n73Kgxgy|0PtV=i}< zFg5A3a$bb9=Rzbv6pHd-VL5JG`4B_3*#bI>Xf@Z$Wg541-I_9E3~>8{o#3zG9vjI= zzHEA8tnU5g`44Yw3q6AcFW`dNkKh4>AV~J3wiR)@99ycnWn;=Ga+!}-IDb}m&pxOl zr~6OAfh8FOTqO<352lvTgO zO3{mvUlC56XTl1W{g#)o5s!eh$2I_Y z{mg9}OTUD73W}I%qX#gZn+e-d<5S(kj#n8Ob=qmUoa+pgsx0he@0i6=1CN6hwc01Y z5r`e6uh>3>Vr?2~2uZju>BK}635+}DCKlTX4UYhQ6D7Lea{D|&Y}ps1tu*$nn~>e; zs0h<4*q&N!{ye#%vETJ9d$Uz%CJ|Jo|4=&oK8i<8Xc9Pr{G9uc%cRD0JgPUIeV%zW zEFjy>|N5trU$%D|8C^2F`449K?>i6|xU2FJs9ovX%rfC-0Y56A#BkN2BPL11UHat`i7`6@_Xt zIRa9`i-Bxa$l@GRyJG)X6B}Vx#{>oTH3D7Ph@+=tPT$LXeb@VhFyV5&`aeW5B(rmZ zC=Xeg50-%gKQy;h9usil3cTjMJ&!5X%{j_Vr#LS^Jz?s`Fa%khT3N|1MRPr5$E(<4yIvWtulE58Uoq%$ z89Z%&1)jcAPl+Q!N+>#;No2`Dzl$JtL%jea;)N5lE+#2KC(f5QL~=-aDWRe(E-tp> zzMY#>xD?kJKZZX{*oAK*$lzQM!vkf+F#N&VzdXRTy~$43{tXjYNLv_Cq5j7WzDg0p z0~qr>Ee5SnFa2j_6k44?wdODp5sH^rStF(fhlbZ>B{#1eV{Pm*C$03*-&sse+lpP6 zTU!i?9Y;fF$t)5hEV0>aLrp z34m8j|7fYb227SWYAdr%L9CQXjED^sZT>dR?eq29mJtWyvN%8mQ@13_>x)+3;#_G{ zl1iPrNDoSa(D01}Vq(!@^@@ZET6n5txX!K+wI-db07RqD%sXwWeb*9!G(drT&^ly&P5j`luvtD1q}_u<{K?wA*)Ka3plKrXC+w5EltG=wBmxLx0uRZD?co4U znKohk{g^x>eW}_VFUE1Hk7%kt+~*LIl4wdoa;<8i@i;>h`|j+9VJ;0IqX$A6W)V>swuy&2DroSuqww8 zDr7ECCKaT)jrx#Bw94jIS3j3SFlU9!bVq(fb~6SZweuBQJWb7z$(M zfHP1pQ&$;^Q_q(1Gwu8^S^7g$2vbILF#F3>bMf};f>L8mvK@ahbkNdf3Qh#mgZ;D{ss#pR)Xf0(~; z($KvbIAuPnR}b%PhW)uCS*S}urbjEU1N2c`}hR%RCXIYcC5nQ5$hlNGy9K~ziR|Q zwgl(M15DNC?j5e^eYk5#mvVAs0NVXY{Rv#Y+SqIeL>G)}pmu%lA{o4aM8A5ynhP~?qmdlDrI=kW z26c`5vXLL%uqi8MV|JN!H>i}5UZ+0^#;IHOC%zs*&Tgw;c5N&|j4a+!UF>0y7ePd| z)0&$D_8?UPSiDeR=NjBKP0jgQx6*R*nl$1nPA)Pwns@B^P~3UE`S$UZj_!7$`nJn= z+Gg=-<{0_OKf40)MigpR<(5-jbWidWlwEzBurTu!1*7K28W8W?~ZE)HD#Esh4ZaHre14*ZzWLI zpgBIIv%Rw5XdDOGj`xcnm|bzZ*SGP|uhDs~yd}0Jv-z1fX(~4)^J_vKYxZgC$OCKp zyrr!{^YwRSgi)sKIywHE_tqQKSPyiSdNZ*8G$3RQ&#h4l5(fi3NAe zp_RegH>FNt2{3;RlYK2tS;d0C$=#69=f-{na;wCl!KA-D%+_Dlv@zQoUZS(djoSe@X}NFTr21Ot#7&-*6ggLo@8fn^^l}-qR6` zjDG_>G+%Q@MWHW>L71XU`6R^s?rf-v;0ApJPc7Z(h6U=f?65#+WQrl%l=N8N>nCm> zqpKehagWSr!NIZW4m=D)Z6gBGgM_48u(M5i< zUJpN%eNY$I#*nlCq26; zZ3P`OiqFseVfU~?zOGTiP`m0Sal0Ar2oabqg$nz?L~t){m2OA_ngQ!%w% zwmF!)r2Neu)*IAp$sm$SdR*T2hE{<(>Mxs@W61hUn60*wy;jz?wf14FOJoM2rhJL2 zJ5%ggRYM4>L6+{J{WAJK)>ullcv}{aYpcw`&G-7qB>h-AsH>8zxU%P?hf=p ziLIXZ1jhpt5`E7P3y;!Ho=<3PUd`R{m*?Y5J-|9byWvqpxkWPtQoBK7n9^cVQu^kM zn(?l0w%8tZeP!Oc6>P6io&M*fDNB_8OM@l0!AqiIkO^_M(V+~d&N@|K1`@Bvr5axpuh#5P3Ut`AO}`RXu_tr?%R8pz`6u@1&V@+y3j+PkMXd!@ zuM3KDAuc-}8zzmG6Q|G}!y5`r>5w2l=fw%mORmN89vQ}R*yo!*_YSY#gag^VnR-j=@Q9;s<3PD>Wb@EW)#N8-#Ux_Pk<>{f@27(dzE5ji!|%E=+_ zJVd?r@kKtrp$*AH_hsH57XvZBgReS7kGPX+=Fms*gEWQ}S%h$BsAdu8Scp;hv{04}No3SxbeqZ^CBJKOP-kuwy5 zl>hm?80wz%{Nz9#J98&SHYL%r8@^F!p>;Lbb;DtAg}^p6fT!I zd)$DTV>SC_TDLCUfyA06Fg-@9{=uG#urQTb02E!+37Bq3ZM5Iv6cp)mXzUR|xLi?c zG?%D?zXI-v-!ZGI3_I1O18vNDal}vV1?cd~0YVxSYx;3a{r!tcgr0A~$8EkVPh0hd z=BhG~iGqZi?+XZLbl<$Sx3MnToSm&jc` zZ6;ss{=kkXzZZAphq)#%ZqMHS`*pvL2cBVmNWR;y~T;J*vEkF$eYqLoGiTm>-xA3dNHArc$s&8EReYnY>dPs+v zHtyUgLPbpco^8a$(nNgG(pbz5`%0<;B{yfZ_Sdr-rzaCb5c-pU(XS@8b|{%|k$KIE z2%FW3I7XbQe1Wx+pr6nRw7yvjR?ri!*(Al%_+U-IS=<<3W41NWUP$k!$944zU7>_{ zkj7J0HzzPGhmV{PXkh|9+&5lS$)vQ2?6>{%W1EXR+HLGt)$ndLn7zL6ypvUk9JNIxsa&h@ddc zMWAB=94T4WTZT2jdA)EunW!cTFC=T_f(x9oOXs;wbug^OB#lkM2AHK$QA2@>vB=u0 zZ92270$4-Az}ll$g_xJ~`q9338D7X$b~@f?UA zx4pIm?w{VA43H{hi9l!LS7?oha>}9@^CtCC4r8Ou@Th}P0Dl5~OKBKMhT|w3E%Sx4FzwoBG*KJZAh~M`c$op^lA7DO1M2URv{F^9&vhvB zmW3M23y{lTu`$;Gf&lI$VNxhEgN5E>b0t{44{6b7Xc*APsn`&i!OGIfFV9C`+(QysEm9bs6vsvDa_F5{@Dl}KeK;kmnG%=vs*pUC%y z-Yupj@9lrKKN00^7GcUg!AJL?(+CW-NGw_!F9-J^{%dRM=E0c`a!^@N8!P{ZdhJcj z`}ue(!DU35p31NCD8=mMEB&;r`<8LN=Z}8o?qlwvyoanV)G>`C{ca!g9o7DBS9oD- z{@g3OhW*Kb@@yOFE`TVWDKw=%nQN~G>l1h%W z8!VL0m}*7KB9DP5d)E3esE8f7s{{J70hy4YdBz#ls`Ak96Ek8i8$mb=1<#vSL8XUd zUK%Z0526^Z717MJbC5Ip3WT0AZbM5z^Xx_v0#kcLA#*b1`edMx%a23)UlTeGKL6^p z^R-^c4*@;zb13V>Yf(diWEViG=q;ILeI(WSN zmgg(UTWYe!+ivTBe(}W%XQk+m!3)@->fOx*3W430WuXonaZ@G0G z7j3T;I-KOLcQDm1=Yo--Q2~<0!#DRoT^OlN3WLZB<7R7-ptFB@CW(t}A2^SPit6{f zph|Np>h6uZEJd*V(uYY`UUbQw<55!AFK{}G87rCff_Z-W1Q@!z&u z)+)wCE6SMGZSr;(lYjRmE**$AFHY%uLkZEBIMLo|?y>~fpM%^7;P0Ck(4cO_1VO#P z?i2p6@H+Vozco>&+a22XSv< ztu-`9T1*xChmr9?S-GQb@h$441~(6+X=il&vYM`)U81vo&1JiLs`2QtT4z}JK*@ep z4)E_T)$0bl0ExQ)Bbc(g}rx;IHvCyCCHxB&UmTif=;fj#>TMbBi*>ZdPAgCq}rx z4Zb6Sw(w1 z!ygjOxS(H8o??4djfw+rOn8pFHvt-4%$;1w*k2!o-M5mr48GNDRErVIqi$Z`;}cTRn5X+|obZ|Tsg$kxvXn|hNnx$ES#^6rSB%IAJn z`T>m8u`@-=JgylCcEv{44??$0NF70r8PpxQj)*}>YOZEOR@VpAeHZ>5j&cRF~-7Ngubyvxt$)`Uw=B^l^xLbX#&WfK9REamj@OAuKTW~G3cwg8Y>rU{cOnN zc+N}p*)Sx3hw7F}8iHZ|iO}srnHySC8O~>R>7ijWY z(e|vQ75CJ>2JI!ekoZ10!ymO=6xeDnF}>Ws?*lzr`=%-)CPZWik49=Xbv?6$zahO*oYECfeEWJ9F4xot1h^kkg)}3TEBnQ7 zft=ohlSsPedYvgeZImy3V+XMkA{ln109Rj@8;_|9Znsd38{Yk+B@sK}kn zukwTlh2;zG?*o~pD_LJtj6)J0t6oIcdfwe@sp(L+ zFhPcnIyYh>S+D@3n+z|vu{keRL8%GaOBK!cGa5n z2M#HH|JnU9xA8Kq(+9er{xr2Tf8=?J=jj!c)GZ3C^*YuSZeRP@O+EKd+&u#Rmk}vS zTxsL%UHaN-3Eor(|HFUise>j%?VD7M?wt#B0Dj2{v2@!P?>m(>6bi0R`+6aW#sXeM zs3XoB(qYh=DpScXv?vkD%s)X!=?a_VsXx61R^ZRnYG-kA@^rXDjG35>bC#2HozSX6*=q7 zfJF^9FQK84wzppB5vtUpfjkVaj?=={G|KHLQAkCb^j%Y@M$Nqgl>0G3_`o}S6_tOo zREHK(Pf-kKbVRN5N7ZH#=&&`tbCM$Gx*?X2b{P|TV)y$SqB@(8E*n^l^tn3HM4O)XP*}9hi=KTS;g1^B7{df4**}xnApcQg`GNTrA~YNVT02grL>{;a-qTM$ zH1`QRIIzxs@|TB(po*LVLGsd3>%lPv1{^(Mt{@@;p$*IX3e*!G6ehkFkhD}~AAW27 zbNVfBlZ`s|R+fOtW2!fSL3_11Bns8FygmOsLptmE5p$c?lcWB%RrTwRLgNMuQ`)c70X+PK9n@Vzt|zRT!mESHOnEiZ@#k zUXdFgLBMK}lAZ(v-MAwiSt?O#tEQzxG6@TpB(GQT_*hh6O54etBO7z+b3q~0Pd@3o zZ^wspQD+F_vlK|B3|%R*s2;$ifvm}HAO-^qj@(%YQnMwF(dGz>uG&r_6!vSjnOo=b zv-^Z190w3#cJy-#l38Wk6yJ0ohyEmjB;>a5uSO@tW zI#Yog02Bj*2&B%axl;p9S;<16v{*{k2lMs;9bvd6a zn(=o~X&Pf$s+kyb><3{;vP33Oa?3yLyWkITP+N?mR{UB=*k3I!DOkc z5b_5m1=mqb2@yn%(l-`XLF0}p#x+p>FGg;mPDBK76zkQ0l#9;L{6{zDoQELP)GRP< z@*)5H^moh{v?ygf1yMQ_GkEjWGIBT8!}F_g*lM5oQ{W=Dx39B?kBxK=Y$=4GLAG#^ z4TJ+r{_?y&z_9|!B}xQpZQ}u2i~~s8q1(Vc<476LrtougrfAS%K8{q{^&hx{WUPOq zy-tJejt$$_<~bePFTd(^kxTNw)@~UZpRaSjrGifGCql=%ITAyysDG_uexlIa_*yNLBd@?^h_#iJ=hQcX8ZMHylFUT<-Qqo%@bYWj`2$>DhTg>bP$N1N6k**A{%9Pi z6OL2Qn>+1rN#u0BUy~68S*A-Kn9vtFlQ@RFnU*)#TnD|#W9zYLAsyL2%s=N2WHApc7Y8~xL6i6wlkCLFOcCQAD5W*ykJpB+>zN)gTPZs^#~eLiN;tkZcE3}s z(yPQsJ(EvQa2Pg7Z#&cr!6PV!cOD4ks2bw|O&SVV(?*(9z3{xk_G;wG|3ZO#G@|mk zU)=A6`O#B^EW7hhzI7a!o`^#xa_b4>G&D6}DuN@Kj|T6O3^$`0mR%O(%TXMgEUL}I z<4f;561;+F zZE<}v-PF=vL0+|sXj@hnHLP4QR&wwOvBYRw(?=VnofcB-09()ppfZLdPMLCEv|;;# zFBS)o0&on7%^FnR=6W4&-kPk?1~b@PK8_?h;O?a6vb*wh*|}xvg_0u4d!qM@2MG*6 zGM(nZ2xbK`bQbyE;VhmtT7zIy|Ga!e49;Bh>cV@h?b-KnD}hq{%J#o6J>Z*-QVUy7 z-Rgz~?ZKpf8vyER1PMT??2g=T+`7*2;Rr)ymnoHKHvbs%!-3Q>^g+aTV!J!gZ;}%p zrg}XaN68b$pYgF~!CZZX(paNhiW6ff7B4)9=kL?(4p-nS>(@9~6*tc-`5gWJsM?oO zEl$SmS4rsd19zot`1xiD94QT0NDF*41AJB705(Ytix_ z*280MWZ&9CyGqhLTKAs6>AN%>SrESxGW|A{%iz9%&z@(S!)*vbpc{^LdPR)IEBx8| zHtCMJQgm!G{-V2Am3_+9Gwo+4KLW|WXrutPl%>xcTC``hHKeLGEek;-UxLVC95VkO zX;!hzkFjA>*9UR@1EM7YraX@Y6xLUUh#DB5lc@GeODTZ38{C&ZD5i z0sjtb^1t>B=Ge3e@)M5T1WJhG!%%rOrOs!>^vizytMrICiBwp2mGqTf#0V-Z)%^-?;0^~elF!XdCy^HYB)BJ+!=c>=) zUXPVzP3roVjEwlc#)M|?=h?!8D^28!1r;2fWCq|YI)Z1=xu1~+5bbJ+bL(lG661R$ zgM1}^G+P2~n*AsK&F+X7>d&9wG06;PN_gys3Mf=Iiw|tU*neDW&Ry<%iYvRb(0FQt zzHM@O50o`|t1$n)Y&#e{)h$Fn(XzBNrk0Gjlw>1``|t!G$^kyzGc4FpL2B0 zP!-GfK%0!71!s8AGG(_PIGdWH1-}$EB?(O0A3Tpr#Pj066BySC&{5A= zwm86o&qDHS6I2sA+(Evg|6Vbv@m9~s{1C;3Qd^s9T+2rnd!@Omy5tlwePa_L`~7;J zoY`5K<0gWOAP!ICW`p7s;xI4kG-Dq747jpcwFk$eZSHEg@V(G#zxTQJe81fYh%!MI z?}?%>8DK!rQ6fjqk-Y;RCJ3xA4_l057O#B7;d=aVjbqBpP-H~*+{)B+ZTWqYyTn0O z1s+QnN5wNhD+b_szEEaXHp|;tF~h3@RnvnDuu+>!sV=&K#D5(BFB?PmJmjRWWlQ%c zoyaEC#GQeEyX-a~o+@&@gyVbjhdSGW2(5T&$GIq6_$R~mSao8?oKhdj2zFFgCPL)Wfl3$Y>VOy<^*M|-=BzvcpF|bAn zTx;WY2J73OW6jx8Kb_$g0`u6cH>PNSMfRtipC+}|Qqj*5;_t(jJPD6HEbm|BPLm!2 z0J!|?LWUfq&bo&nQXa)RS_6#sMlu{kpdFdHpRCzasITO@KR^yhr zM9Y3$b(zcbh;0*=sOkBa=iuf^4gycHLX8g*Af=YL=yCdK0GG4RLmm%mYvQo=VvmLf zYc9Q&@vRz64@!A>_uftdPnp#R7FC=o;Z#((N|AIqWyoFk9_9zmq6f|kU# zx?`mnrM_5gV=phb`cEz`fgKIEJoFvkv;LG(IZ13w z0_y1LJ@Vi)KGVL04TVB5d#?4t+$R`M92S2X2jXAT=S*Uo0bWfeVM)kfZwF)14)v3k z^$l=4XdxjoxpO71*2TAtj~*s${yA-|d)XlEki#x>9B@_nr01$Z7LI2Jmnoe!;f!6! z>~7+V^zD32@nF=^<JXC)W{{>coqQ@^cJDh+n1voPi4LT2ersspyxRrS! zjBH>iM_jMRhfg63e~>5p*3V$%j(0&J{}^S@2o17+vBr&v(#j~xeX?YiH#Ki@j;VT+ zvHLo$eRrj<((&ZfW3(mh#DIq+R?9*!ekaF<0Saar&W#i~ed#{!*eR6oZMUFnFYh($ zJkIU_7~{|NDZG3gk@fF#7&R#9yM)Pt`)E7wRU64NeI^M)-^3dIH`nq3y8Puh_w&hO z%CYZJdI&dk!z@*s@0%L}=(!IwcdK(}6PN)h4(qs?lalO2e1zc&)Ft z|4_x9((ysf_+11|zaR(B^>OQ`ao<#9dgL`-yj|OXQtPbz!T-rE{NIQfbSYEq@x*<^ zMWUkWeZVw;)+J~03P7|CNGuUW@m+XzJ39j>U`X4(z=u<5!H@I&Y_$0=Pj&o=3VurgL_7jO~FM2dda%il{caQsB68PqDX5V+Bf)mLLP((6|CMU6lrBx-Hzon}W z{|+>=i^`7*(-P>1a<~L*gSTywtBWJ*IZs>eE9>5<-v?VS>0IeOrWva(tY%skK1Lne z5ykH@H7sY6CHSc>DX)fN=k#SWZSIEiK!({qq=m!LH=t;9_0H{YE9q5_6wGx`lbZjf zS4XNo2e+;w5Q|=Bv*j9!g#tj~2y?cdY?a}hwb;9tw>KtO&{pmAKRK8Gw}HezS(pD` zPdO3Xb;qCAd;U-J2~a->TF#{yr-N`ttGMH2?C1{WFms(8fgA z3sNaoODpDCA6s{Y`j-dz5BajNZ~{a^wVk&cBa*6k6zcpERP?O$+c1Uh?_&s-VF;wSWYEk+oCT>R!&9#-Rt9?ko4dT47{|DJ# z!;iMXf>!JuSP@oBHNzLFKw;|Z6uVO8%R7?v^z!Y7=}P>Irz~TT_h(<09M*s4kj^&4 z_n~+Rj}60sLfxBW66=O{_||wM+;@1pS>iVC4eJ2bp^a=_#8rJJ(?DN5;}qu!`Thsp@kZ;F<=%WHWn$c}E1GhB^7VxA)3vs)-#f0&UdH`RAKEZ8c zw30vBLKcRg(+7ojv{rj7rzkJaw!FFePIhtJ*%Vw!ft`!Zl+TmLK$+s74iYFU2Vza$ zmp}lZ#09Mi*)C0tiCS<~3uN0qCG4sA$f6b3NWSV?Lg5m|oqDdU!bIU7%~D8J;n%%o9So zgsb^iUFn#`0my7*S2XBFaT_mu4M1W-Qi}`?}4l*1R=5|G|TkvYrNiZypVONXHnLrx*nkz*e!m> zYj5XgHQ_Jgq9VDEJ}i0tp8lxMlG^M8g9a$#vrxP&a0e6t@URP~!bpBv!10?}79G75 zQuIjU!@gvrA5DpFk%BAEufuOUp5b$>5tX`iPM8GgYQWblnVrb`{h%a*WI~>ao>0h~ z5(z`>;K)yEfVA13au1=#jH7lPIH@V2PJun7fL18!h4} zyEQHLVtsCamU}O0kAmPd`Oz{G(`vp3?Z~VOia?yQ>|R_ylr1@s0%}fq(A?! zOX4q2eiG{Q+SKn)j+tHf8u%wj^gkjuaBWvDnadoBXRH?k$i4GE%Wtr%Eh^>~7ZU&T z+HXeaRZrknvMv6Tr<_K)g0M(+86*c`feop#JoasT$Nrp+@MAYpkOw&i32ciiE9XGf ztw!?dh$?y=2EX60K4_z$b*Cb0nDn=oGj&S^$d9d(hzN0gn{jrbj?s@L3DXivOh~>% zby;AN$ZZyWb!(>DVmIzKgqh;EW(&0>m6**UoIVp&tk$d-AnU6oB`v@2f>&uMYIBws zd7GUMLYX?_SCG#{auY(qTzEMOf4qBgA@9QEn7Q$NBHM{PW9$$+vCNA#Y-)MHm<=m9 z_Eo`F_J$2=xjJi-Ak8@q`4d071?b{Q`jH)>nya}RKS_E+Rd5@HVFp<1d>l!;CLNRn zPGUTm7)IDtP^e7lhYw?0hbMzOwak~cLMXH<$NiLU#8S|5x|-f6XZM445*l0nA+can zWpEV0Dm|Md(M8zS^Zs@zezGEbf2_sUk4SG84Xv0PfSZV0!Ke$XgNt{T|GJmu`HHlm z#soQgez&PY)W2>8&-`(h*jD?5#$0`{H7)*+%c9`~A9vzIK-wWglqz%pp+Op!U7&$^ z7*8SXz@hTzC9ZtXY0|UzTwfZcVsy6<6`z5h*h&llja;lRVs@YH@4ZQUmo<5wZh@}F zzwX`y=kM6uAET*mS9;t`!2d(odq*|(ZU3TtZGZ@&2q-NoO{J+c6-ZQ6M8v45D8<;2 zPDD^zNR(cLD5wYsQ4tXkBhsXXj))kkflv~PqSAIKVMCJbTb}#Q{hc%3yW{@B00v3e zd#}0Xn)5T~{CqIyz;>)8^{rs1qWQ{!MHj=HxbB%6J?5tQM@Fvdo7fmnnaG`pRpgV) z$?z(RVCIx15Y?PiMX>XqYz4&MsY1>{HQo!pI%5;M$Or>yot-3#6(qTn!pgPdH`29|dpKzDENbLh(@$8SF72gw2I zGwzOO>E-lc@T@yMrxL{CFY4>hlpU?FOWTb7$S6?+bz)iySfu+1s2fJOBI524%JyO_ z->SX#%^VxiDR8l}ekw{0HL zzE{mKAw?X3WOuh|27K>c9t}!;Bz$%TV+HKYrRs8pav*)=4F0no2ooX!+uW0M58-Jh zE>wxzg^0dx_#+a^oP3v|l9DDlw)v~xyQ_~}k#&#ZhrrPPh)f1k!pQE7e%?}Ju9E3} z>15Smy(Bzs*ipR|M}%o@@?Wz)1dgNCbCOK~%^5xeG`YDps@m0o!D5mjsga~Zzeq5b z9CPh4x;jT}?hLxom7|K8^81wzit?{}R@C`dg;}^R{{($&%=Mb|rKiZK7p;NPmp-r&dKvvd(w2k=a*NKrj%gibc1B@Krgmb}m9ii2$TG>D~)p>0}qXg;M=ms^Z&gcB2xo^Kb zsGN%02r~MKRGuY@9dLMfF>9n6-9nZ}R(oDjftj3m3)#>!J4R|cwv_X2E)LjAG>lhL z9{xZOMlvv&cLY!arI;)8_d5l~1L2g>_G`^-*(%G|+X9P5v9aqhS#Nylt?^gAJ``Eu z23ZXs!2j}|Tb#D{we$@Q-R@jC(;wDT+W@kp0J zsV%^jD9Iq+CfW~xt{O_zNj*jHHh6sGYwSW8=UEHIMISWX6d6=uh8ri}%! z8lp6m1wVVSf3PMUl>unr%9wdzd!UPM)L3DWjVA?IuE?U%so*r7?>-$YGbkHL0gv+3ea7Ebng0>gmN{{+sy0xl10ZS1@~5PBECiN+ zK%J+eKqI*L1>~~b^LL1SXvm(VNS3v+Lu+!Yg12}GzhEB2NAjJur>Gzn&2)EYtkd-; z$MPOLKR}(t42^Vo-3uKCc<5~2ioQyou;7@=g$%~eRdj8@u}BvCiH00eu-+?lAtTOz zp$A!ow5A>CsnKwsOXyvhh&LAN*Ynn}TY92#{}-`5llDnem2z z6RaaO(Zz^@7c zHF1bz-=h)@DwL4NY^*UY3?n6c38)9?(#AHe#=t$&l?<{9;}CY6_VJ1vai)~Jo7F;e zVwX0A>34^hJgJr&ExSuXlk3jVIi+b!Qw;8i_u_0M`-5wEv9k{$x{UX>H<%G$@^m;* zf2Jqh<%#C!tQ;0L-4f{=h7&OyaWrzd_pw~=-8;A|0A`<_MvSIB@DNq>>Zos5) zXwtA>WAH21yT>8^P~){^mxNCr`PR1k`du!ho+?U6?3^ZlW5YW3Q=VXlnhC*z`RL$a z9Z)!w{o2wvtl3ReM@5r%5_fgiw@OtL#G9%a53a{gE1zu^vp#cu*VaRy=k!1ybEk0W z!-t}-x1(JP{A16{!ZZ`bw%*@)G;Wvt*_$FFASYfgXaZLl0;ivV!Ojs?CiRgO5HY8N zruV9_92^R^Z(8D=Z}`-0Zi`APm(B8az2{BS8{DN=zW*+UkWr<>w*iYg2s$YfF!=B^ z@)UCf+fP5-tr22N7?o^^oHa7b-)lMbpwU>lGCiqr9gFvE{QjO#&!cJGcv#C-AV(j; z3FLHm4qaqdd=%?Gt4WY@%Oi6S_x6ILkKoAL6N-%LblpZ*kC(YOSTT2tq%YhJ6~%vg z7s;>CRN%yXmzj;c3!^#99%|KH;MpB>~vXVF1UfvqSci`q1dNZv_`$klJf3l1IpO z3^9*V&8Y?3LtnR#_bi@|I&{V{SHDl=)+6OJLt?t;P4Ih!N!V+Y0iY$G4vfMA?4PL- zz5yHpkRw_8b%TW9j4xsJW9{Ww$=L{kdaEwHZ4=XNl@&{0Cd}m5m=p{O&-f|eZr6fy zC+rGoh$@REpkNLAyGHCdwbgKkt^4h%g=)QiO)thdwu{?Q86PxBhNOp~gs{qy)0<5xADNKwk`s=nDlzlU*p0!Gku6 z@r!LJl#{zd_H=x%tfOW@&*0KP>dEyt`cXqcBDy&Xo}iVyZp5I^5E1*umE(B}DK$!i zmzuzM24-6sR1-^ZW12dYKKCC;nb$b>xoRGFvuRLkGwYD_^}KJQV+I0QHX>!oW&dlc z3x?hKt4YmZPG(kKa2gIDQMNej(VjCn*zRWAoEzY;6*t_g6CYb9%$8Vn_1qm7+@tL@ zaL9$(k~+|3(bKzu;VB*ATK$y+HEn^qS_1Gz&QnUFQPT@%1$(Ws6ONmP-HH3*eqG~9 z#r-GhkLpgHWdbofPke&~ZiFU~$r@=di>*Q9MuZ;x^UT-XS@cea&fgr#OWdnLmU)$e zdxxGdK8vW>qepElZVVt&Oo*2qG{T49&Vc#)+!r5e`0B-mkGfyGZta#ol^vGUcqP@! z-nJI~r)wo25tsA3GHL)DPy`zd8t){3V!QO1@4i;jyyuKg44am9@ZPlZl0bos;AVLM z0sHVrX`P_Ik3pn9f-O+T19Zs({BVA9ZYHDy>~Co zRZN4va-8eOSNm>jl{4~wG*Th=9S>bF=%*u6hnR@$Pv9T0EWc5pbA>|pf1hA3NTNp` z6-nZbCyjc#y)MTWJGQDcomH3j46EF7OP=a>6E%n(kmIGo-h3tG%Ow-x8$@qNY{6iS zo1sU*?$~o06%W^KExli+=GFNq7CZ3Ja}LJ?ux;3_NqQ-8DvG=(;FlEAz?1za*qg{6 zwACIgN*LQwW|P1$OG6Kn;Jv!F(QTICxqVCc_z~}C`%7+7p^H?I*O#!TC-{;iK>2%O zR;1nc4bc`<5+z>*(wbSXi_d?S=$|h3bRSpTAh#aVp}TY@PyZCFCDJU6`9`1*B5W&x z0oT#yTh(8z#Y9LaX$wv>i{tlliaV+-a`j#(XV>N`NtoTKv>u;Z)f{J@T{?1hjdSro zpFz2!*g#;!6l}gum6_<)s;DZ0tRe*1+;;!(uR}5nnjCICo0+oK{pw9@Z~R^s7R>Ah z>*BdM1zbR#6e8BNSp_XS2I45^^K^{B7b1+Ir`kZ!UQ6nyf0%7TN7d|rW>Pq4maK(Y zt@mibo)C9JvE``q+L27dojco=_dRaxK&F?H_Foq+gSR#Mf~7Wxx7lxI#0G=*rM5g~ z!n1Q38+2VHms5Uw9ygya!de!T?A71hO1n<~e$B2i3R|N( zf85y)+0YM`FSzEVay`Cywk1^E_R)_CTS{Z_^?b^VI@uM(} zl+`h(buLZqexwb3X?Osl_Ep0BbPk9SU`_2Q{5uvk#w-Wg73*w_kLA&}s4cd&|~bd}BLUBSf+Q znvE#tM@zE=8t;!BGj07b=)roVzq|lq<&qPCN|K)rk?VU&BG3`qy@h4bet*1#W&)l6 zpgHC-e4B3*czmgr4jrOy2t4!M#MvRxzE;P-98j{E@?Jl#BC~Jvi$7CEI*;6CwJhh#_)wkFm{#&V9+WazL#9uj zd}}go19A6^eLEhqVaQCHhK z^*MtPO!g~M7qAJyd+sLO@6zo*yNSdKUKc&m_EW!OB>gDVn;bYAh_A5qGb#c#C{ez= za82ML*1p*vUBjU?zw2BjXab4Yq5~24=8V zMVO67V&sh{is`Pij}cq$#1&eH9v1Y=3KaZ!>P#Oa18H~CI$^%R7%-ZqCA5UUjy}X~ ziYy+!(fYcGyx>{zGoh%pZ?(}g_t6}G&dnQ!8t|LLW60! z(GYT&Qgos*A!^tn^9p?=rwNa0fARa5NyB}x8O!}ZGHhqKKm+bMtn?R!3&siDA;ppE z;Z(h;G@uR>qZGC=JcOKc&^1R0>|GoOqxrYbU7LDy{Kf?bOqZe<734dX;LVOZ1050Z zNxsDZ8-e-+GEmhSF@_Yma5||P)P~PnL5r^+u8G%CuWVPtGL|N}StC}0joPEHGq~x9 z_yFWLf=!sQ3ezLW!oldc{0;5$*^5Z-bEg4^0jI|Ggk3cc$$Rb$0~b2~m3}2g{jOxN zf6vYdsK=^4$;TRqkhP`8mLT!;oVk1;2%>JQphum{@8(sP!EYs%q! zd6q9=#q-^-9^^OVoB$i0UNlZ?XAIc$+Tm+_Nr8C>qDegUp`yH`*WB6l<*PeX(#G_V zHa}!)SA!w26LWe}weH6AvdLc1Jk~oV|Ku}NCYZ*ySL(gT#q-vuJ1A;>F9G?jJUbc5 znPt~7zII?|n`5$04`1&mSR6e@aur2#}MzwW)q8y zgnI#VKll`0OF+gFh%;6ymIsYrmlGN9Tkx5($T)T(48=N61}#-!%rZ5Pc;=fq7*phA zH-4x|9l7XpsjCUuUAk=A_ND?O+}4gv^d}2lxIvon0s;vBXZKjqFdORah~+dokc_JS}pwBgwG zwb=EY&$gf6l{>u!@Zl~{`%$Aum{VY{h#Dc^@T<+o6z*~3>v>9%c|lZNR(3{`Eef2S zPRZv+>6iR7^idm$nBFR#TMlRG#c9L-xEUL%&jKy5pVHw;yT5rEr=lWYT0I*G)b6z*C`l0|T0+dk6=h%p#W&q4L?DMRD_|dV0iV>a5pcwlf^b zFIQQ>Wneksii(O<$co-S7t*rkk4Sx0$-(k6V$9cA{3+blB%C3CaULa*c0KB?h=|Se zEiRf+dENjj(|c!L{j}xTtO>yH_`pnRCx3O}6&ukTaF&uESHLvH<%NHDW~2U9Bn9Ay zVc)+okAHpNA=-UrJhn}ny%R-SBf#-Db=?nuVsUf(e6j!VE%`X4 z3B3yICFwariy;~Q+8?n*6|YfuA2xyfS)`ZWHr=+-(i49mr5W=~;m|(TK9vckj(Tm{ zml}2GPRgiR%$a@0lKUl65;1Yt6CBY6o=ldC_6}}1hft5EAgUjr=w@nknrm!vHP*4H zS9i+T(^vO2-r=)m*|Qy-5BtLm>wX?w6ZiY)JM>Xu_TRoe8RDHD`VC2mF?^H(M%~Tf zL~v#iCqC3eu}61`eWQQ8RPx785>Gv8uiVj-YhoWrjmB|-*c-BbEg05pCYeA64Kx;E z>~;DG;FvrAW`!I!MJw>pK)z)tX+9{rh1o}87wA`Abdu2=is98 zDi{NIj4~6W1xNa0|M^uB{yybmxA+n1!}2(@?ATj<&YjKWZEv=q{};0wH zyfc8ct$Rwe;Kv5{up{~ktvPaT7362%MsmKcDR-JupZV}?{(u?-09*eY38J;&2ef%s z!lw#`d@H1DSOikHp=?7dz%>k9(wT!>w&AuFjjtk|3S7f8o?|qT;e8-~eF0HUd86%w zC0@q)O_VQl#2q6Uc6JHT?f0DydVO=%NvOGT^t*-#NVWq{JK?6r_?E=&!nc@puq*FA ztURjSL)0Ff-c4ArS@!x~7Ial3cSt4g-TloMkE!QnW~~E9uN!T^23*v`w*>^c`z*_@ z4zt>EEPj*V^r%^gwNEwi&G^l^e-e*}CocT+s|SC!v$&mrOF!0IJQ>nv6S2dhq+3Vh z){d#qwP6+szElyq2dck?mkYhh+$T&!_QSZA;KPPNri#c$=JT3k8h=DGZv_F$rLl~H z@Gz-;?`H1)9gJ8CQQoH&H8M5ot~a;kL9^*mu{u`Bb>Ai%KxMc5*ZmwNe_l`87izv6 zu)<)HFR&pmE5Y+w4n_Edx{~y}1cW{Rx0ir{r5PK6=b~u?;K=6pbsT(Ib7baDloo1-4h#4KUJ>CeP)l4l4i~$jx zFCNuQKitqs*O2*sHf856*FU{K>74f_9f8*n@rPrL&`^Y}!u_-jJRa3|&H0t~@F~zj zv5nR@B)ei^f=F;>9@^Ec2tec=*3gId09iObj<<4aHifU$>#o=#%j*TTixye>#j4%! z(6WJqlg!S6&yM^HKI#;HdRJ)ZIuojeUu~O*X(az|lzY00Ps-Bc4h~TLL5yCG2e3Zm zK!tZorH@TAfOM~d^EL@h*b(B@{;H^wMdnY3T1}lA%FkNpTjG5Ku9Gc{6F#gPc;Lc08g@^cUt5Sz|8wr5YHf%KA?|&eK5jRHL z(Um=;ma*y3tCW2!tNFG+O{n4+9a8iDmd6`yK*~(HqzZ9-=a+X4UvL6R4jT=UERb`Q z2%i)2TnZw!4({Mqp6kXVGU&J(IVQ*tR{yZ~uP%j<96)QzVDcXf(iy+O8)-M} zu<&FzKrN;22u0UL#}fUrhWi4;>&^rf(o%9S3}%1PRQ+@~;IRMjclG7Taf5ie^xw>W z&0((S)IF|eqne76fQqV;8+ArEUP5;n@0A^$n_Ky+7;xXZHF}z2vTU`3PX9!iPraj) zv^zR=X&>+68NbJb8FgOKSlLt=-;~r%+LTru*@Oz zx?D(RqyefZW)sL%_k*4bk}SARDvO7UO4Gpq^Ui>1;K>a+SIJ!$e`DTr9eT=*)U)5l z#p{$>e2do{8{ zP1{=5pMPfm{;StN%%MtvpcbIFSGhRKW*wd7edaC!Q*Z^SabPoHwL!#KG1sDcsNh-f z@k_$ugB!pRG%w7V82%%&G!*?dIac7tkAVBYR79 z2!AJ z-%ResTI!^BMeL5{G_nUautU0`=7M~u zHi^*0ACBT>4I8@ZHpF@w4UBltzv^YA(25GMoQLEOYZtrsgV@L=*Zg#@E^!bpI@Mj$ zGDrgXO@wReWMby!_xO#b7UZeaA8ku^FgX7Cle|`7I;mhiu!s^7Lx+IKsW&02!z0Oh zy-l`5U=3?@N;@rjB1$pfkqn>6h?$iCM?)!`BXrRz};vR{UKsxxUaS&LPyGewX|2=t?N| z68i=6sb)*sQH0HPt~xz2&NsK!F$G|CdEUg+G}yW`0(FNX=@RJFyeVyQwsjs$ZKL%& z@^ygD)CfDATI>+$IdVQ>gdWRuSZc(^XKIMwLAG>n{YVc}_BLVZwii@7w9~&ZlHYsT z&Q5sE)1#=s0No@#M+xz_(Xci;7R)6WYy!|}@H1wbuqDde8kDq#vWIE=*8P~fl2d22 zV`7iCfSc~Rc5*QtSzpLEAhoiK0%GlWp!vUQH@0a=yZ%Q@1GEX^tDUDVi{6 z`mPbqEQw_vL!lmC$6ue_a{#*?1DJ?_GVnPaJb?Hp;dE8vW@z5B^NPZqnvL)|PQq^g z_*JR6q5+1voXyP`j4g5{dTZ5#Ykf2Ck&cGcnJbk1Xx_6&JS(ew7>7uyL5YO{T=nxE zh`IBx^S`S7`$^kCpm;Dy?dwy;GnbBoTwE6)P5TDqYEc6N9OKf0F-jO-waT3ahm5ww z34$<1ZzwUc4Fq0WRNN-a=j3gN!mji8E-P)q??Gl~l$N!DlM<*HfW7trnxMR-Gy|U2 zS^0Pq{-x{}fejua_XLxZ5U#|xQXMhtI-4}>D2Zy|c zuXb^$)2Cnl%)FBka3z3O>3l`qY57~_$gJK9q4d!*@6FA0o>|7u`s`rRXM&}!f;Q@I zqwvOk3s7h-93`&?r~!esnztsrky>V)5>ev96$OcK;`upvTRm&&^+!5%-QM&RTSI2+ z@YYP#;}^kcd6I^Ij~2mliqVh_7&F2AwotFdm*T>w7OnPrD>vB_vYb<~39CHBQBMJK z4nlarEjrMae@s@Sdrlvt4iA^ zEW298ckV^svY^2__y+nKqyt~dp6cPFJus4;Gmdz!Dl@jq*}OC_+4fbRC6b+u#y;L- z8TgD2X%o-XUFB}{CEL&38G|;NA3eAD6MMHDJZM`;90rIHZlu8E_i#|nP9Q9A`ptj2 zb9THubMp&&Dd2~lm;>&0o;C`FrDs7p{$6uP$O!zxET>@3Z>oKzE!E- z9yP7$^ujPv4-M;+38|(pmO(BdWE>|5gYRR5)arwU_h z4m+Fxaam= zb=_B=)}6lzgI{@_tzG&RLl=4s9D6KE`;cvA{7mtmh{Akkxo(ti&diDLE3Jt)u`AtI zf>)oxvxJGncoD+1< zjnkS8U<*gY){~PXq@)1;#-pCRrv2;xH4D^3A7>)#$$d*?0M*}y`V9p4ugzPm2VMZ{ zf^}K<5a3YkXxU@@_(0S#?6RhO@Y*0??jTXsDg#GNqbeGxQNE-dpp~1?=}b=Q={I}Z zoUTZe2ysihIPtjtm0Ol>W2T6RtS#A?3LU`>7&NgJu$*ykOI{<;skoB~)Xqv6N`V3O z!9Cy09wkT??;0MceLm-~*LwjLZyA2N%C8boML^C}mJhBU=mHvZi$@~vI+oC1&9 zN?2x!Y={lX-GAZeFX||+c(Y;OQ5+E4yGZ&*{*VK_G6(eS5Ww}~`2m7`-wtP*z^JhV z^r%{%LDsHzz1Q@c`aZhXU6iwqCOPl#@4o+t@(HRENH=I@oay~j0@+H86iz8@8BFU1+0Nwwa)3R1aD90tkB+B)FO z$OBkTnLy68nW|uXhNLR6h9iONg-__wj8i_80J)|3<9t_^rWH;zEB3YJb{DtsOn&0FyX4Lnp>WnK<-lj)-4D{qvd8;*hs`Kk)Cz2T3-r%1; zsA5J{1$#3Ds`)SA6ssyllQj0!U4sIjteYLU&D}0X0{d=$Ip`a56NGF|bX^FwZvv6h`a2+RMwZqG-dT(?CFwpAGJXD>*PwCsOrFEp&C9E;MRw+x zoi1Hb`b&QMFLH0LCl9m>gt?NMsHHgqRn>f=p70e(+Jyex6SP%Z(Bl`Y_p1i4v`tr3 zDYm|Dy4mT_t`fKQt?%nb7rXbe;;oo`@O;6J2?JR}ogu(r!86scZ>B2=q=c~~mB4^Y z-^|r~{0Y(xBQX0^@gt{0rkdR^UT-+K6GIqn2`=!ZIRKgYB|^^N@4^&(|8>?ty~~^MDyZ5Mc!t%ZLFBYyvfMXVx7cCv!Yw-i1AUqdSyn0@W+vxWIM^whwPB7UtddoNNmJq9jRc zNVw@-bPaRCT~AhW=!44u(j!7#;#%^=gg`wq3aCf2`z=SwQ?dX{nTW124g5S`iKES5W&M`Mspt@HZfMuY+nVN*8hhj1+`|uX8Yl+2&XdgffgAATp@cO=8@?Fn3%W=< zTz}%*fVB-uI`C!ZACc1^dlPt6?^|n2N~%gnjy>VjV%!6a^qNohEP%d>M(Sod=OY;5 zW}v|BI@jn6b^twd$dJ>Ud^-p956W{_MV9xL=sZ6^aUnFz{Z#tFzIJr##`TFmHrtTi zV($@UwBw>F8ql3bEKZrUq}wlU=-b144wLQ-@sIwfpFr(LIxom@@ZeH}qN$$SibFd*~>Ks4D5GY#ldW0_} z6x39Efr+hQeR2dBa2Qv30^HE*&SZqYMC*8OG<3pAW)zIOLw>10+0dC;)bC4w0aUWU zv16fNzhh+zbZ+eJsl(0#SApm6*9pOaKURY#i^R%;i5iR5;>`G5NY&gnpV8@WOxQbvha5RQcf)n@H)gPGH z`_CT{HAMqdS_}gjLzm-i95>{h8(j9-yYev@^EIvf&$k63nMV|U1bM=1LsCB#Izv%m z|C$M)ht)`fjt0G5+EyiLZrDzr7a%m6#PY(08HgAHN+*ylH z?iDssA=IhXXYINlW&_fLwZAe1@(T{HH_j*OP~l`wJU$jLS!D@LoJ!J+Zd6kJk!rAH zUj8DG%nM#wzT$o;ejKB}FKTCYFp;l`yaf^dp5wcKjZzdAPyUU;I>aFNE)hG@##4OgR@+x90`YN55dyxB%aBJE$31 z^H{SzvYwCa&lo>A^|IOyL2ZS3nNqzN&}wLcQSNVg340!aJ_o*>JrOR=@-i?JN7&My!-fyFd#fb#20u+-3DFyj4&|jA)(;F{y(D^n zF4GC?g+xunUb{gde8~u;jaqW5*)f96qo^1)F>X#|rUYIQ7Io}eG&@_;m#!XvA0KGA6(r|0sivFx+flpledLJs z>cM)+&+4sv8n>42s{ZO~m1!JB2akvY5yDo6N;4ofFRrvu+q#!%mJb`?E8V(z^8vh&=`B8b=0f|27&-tVnwiL(9Hl{r{F zo)YHho}?(&X4Ivcm>6;FtQWr_lLE;nOadebkTtCL1#4RbNAv82$%2b~Hz1&r{*l&;pR840BKz=AH&Ilr9^cuZm9ZQ$t)d2YgI zC4>z)F6w0TNv82oz@^fPOfyrr;SkS2y?~@S;MNUvcJ$585wP0dms;5U@9_OC4HeN5 zRn9I-F#T9*hY@|_g=3bgW@l?`3Jy75X+F|kEaTvKGW_X{pqC_H=XIhpq5_2+M1CS3 z%XtMn2pQhaZrDDy->NAzz^3!I^71uSk=v4KFtHc74|YipO-I?f(;pp=hVTTIL`x0& zOND41d+jfbc!z?0z}$?_9j8G*=6I?>7kWBw7VC}g2TcU%d!&uv=73LkHBe+Fj}q2{ z_Bo0`W54%w^F%4q_WpgEP?RtI%22yQwkit|`<`dZ4X8r{M8?bPCwm&Ji0t!ZE%@!3 zHgKIF{iH5wq)jCD^gKBo=~XYg&(QoVv}_j!$o~QN?=5O!OB{ZL4Gx~e#ofmLNKzUp z{gtOX{eRvO6#Z7ZR49*-A*Hz=6%RYB40(^Z*Qu+>-spaH9!+^wV8Ga|K%8CLF)-58 z7nsQM6we{^uLQSYwAd;mbG7{b=K)BItR#$MKi_V%Tx@%-v@dXCX&ysqEDI&c|TKTjAi4*Gdfq#Ena{^R%9Z-H1AOxZlroXjzTbKNggOwFM?p1&)9Gz{Ex2+kZ8t z=L5eO9$4SiToD~zB}=s8;9}-IhHnoE?;E&^ML2n(-dGyh3A44+y}NO$pK$;z3GiM% z4RwuS8z~W)280N@e3hL+jQ8!=K~?Bc+w;y#dS$}f1r3XLmX~HH$nD8r8&}YX*u7kM zY!ok05ryWP(UOab-A1#Lu&F~Cd@o?_=&RF*SMI#{?(m+xNa3ZF3cr}Kmwfo5$tH}G zrwZ?Dy(ZfzF^(Z6nT_-ICAmkrs;AWph*IDnQQxeVff{MN`E!T6=s}xHzTe3Ik=XAL zSECmgjQ#J~)U_K=A3*<|di_7a1^+eK`ftc!Gm9=q=Pah26+T7852{mkR|Vx_YWr*X zO7_!aK@qJ13tP|->y)WQfR@o^$UoE9FWe$5Z?@`q)&qL0$)MI-Eqrw5!e2xnipx-I zQCr;!)^la!IW`juWvn4f5KXd>yC&q7tvb)2qYQ+F^lJc_DWle^;(?IIYjbszk`X-* z&mfxabjjudP1(9Vwi{!0b>C$>Z-j0hJ-9|zCyPt3C5Mw`Yowe=46-z$?7n{@%xB?E z@c?bZ@7}P{PS0cYF{9UsGZKYjj4f~WeQLj5d`eV12-4>C%+^Uz``-u*JDx*pnNgb1 zRE_k9LYt6(vaSCq3(<_)lK0^0g_e?=tDbGJZ^xF<1=3}R_$;8EUJGC08wrjMusJkX{W z<_F6NR`b<~l3nJ8s-Wl-`})0&)9>JiU#A8yop5kJV4kPYms-CwQ77b-Fb;bIsG86} z2&)#+QM=o@cv~nIYKo3aKU48V=}OYY4ci87c%e?c#qPl|2x%+;McSt@LD*$hCqy)M7di zX2J66VKj}@{zs%Z`tB$f8&-weuzNCCgPA-Ke{Ulf@LH74suX@`+vu}d_1N}(?|#RL zOcc{1(UL}l$X@E2w?v6-s@R2}zHYt4%9-shMb4PL5j#AtK9F~7^pNvhwQCp9`31wp z>*yQ`X&s!#RwM%dum*uvO=^z$+qCWQd3*J`=HslwOskTZFuzAJa*>RoO)H~uB5|KT zGfo!8aR)*BOvoBv^rQ2cvFMr9wp+qU)6B;OSrb$1eJ?dzsd>4##iY!9&fLjW zpl}!x9;DsO8H91|RbP8h=x#*p>Y*9-QazJ{7f7cX)0IhI5EJ36s&n4!J=hy-^gPwJ zR3w>mW&^tjf_3C#>Z`{q0OD9%2T!>2Ey#76KuzOVCRyJV4hqU*?c2P=FL$cziaNq_ z&L(5Xko5pAL$oSNZ@9DpDyx^YcYs==^|tZ1IsYv4^N^0`$5;y&%1q~C z!`qP$yM2Dm1sSibbGGjv?HK)GG~}dgHRZ$kviEQQRM>8T0t6-iu`%=#3ZtYINRHZqx^DP{SVh9tLV` zQ{>#gN8)gEH>p#>d_x|!_}o4rmiBQ)=d_UW&U)Yl{96yme=%Cg`zC}5WDmAZ zOTZ1rQGgrxgjo1BCjLBagx>%s$c$zPWp;svnc0PR^N18%m`SM81dmn?R_C7%%+4x? z6cdP=iV?Tlk7X6#p%H?7dDRoILf$UQ_YXdGxw7OrK=+=#OIqE7swuhC!7lLc?Htdp_Lvei1g8|2lb0s89Ov#{wTA|aFY3a?-; zvEe3c*{gcnLZG_yjQ|7R zMok|Cn`xPA3Y4LD%uZ3R8~qZXZw{+)lM{?CHH_e89UE7?XHxSHTg~*Y3<|R~IHm*F zk6oWHc$U+TE*2H2X7TaFBf`usYbc;GLD7m^Q~iqJN&8hJ4If`|DNU}*mPyjqJ8eHM*eKLoNS6slWvWmrX-o40yqx5vnGL2{k zZ8jFJC%XsT%L)%gJhuvp`r>*wCiN1iwuBSt92&c%BmB7V4H3;Z7f5$xvy9V+6h|ha zKwGraWdYRm(jjI^&@yfle;rit&)WU4(!D%Rx8BH}T}|Yt zZjD`~jh$J)4Ad>$!DHMn1VAL`@h;pbA@Y36unrtazQXgi2U9^HWRokT2X*ieDo)tS!Ger8sS32D?`k@>5{E%Vu)yxt$_r!kj>m3kx{ z;$gUiJHeZWsT>K=fEK9DbAUldjxy(2aQoXQOZ%BIG@~M|tb!&`l^b@jydrUbmTkyn zprK<1t6QKYM7$izIl6PT)6ZKTUKfPiAV1?z=tn&Z1_Jp?0OcVv!1dRVHyYtsa?QO; zOZOLM|8cL7svI@j?ksmc&HH$7!*qz~U{KPLfYswxpx_bqk)noy2rQC?9veBKA%fhX2BZPXHERX-ofr#*fouS}E}is##29*#PP-t_f2~~U=41Cq z_@$~dE>E# znR}|&u>|4RhB^EDj1H))>rvpZBbs`|F*C}4#GZeL3wNM;?^7IfSFOJHDJyQ%F`+si zS-jI=vrJqDo;7Z&{&N1PYnz;S@2Sl$Ik6&XoM}6O+T*ztAm50rAA$f|Yr%*FS}I}B zL<@S8x$`!bz!*qk*DmdUT^^*fe5yzzWLVkHbY^L4cp~^v;v!h(x>GDg*Kf*Rj(tk(nX|jn_cS`RAT$#t zY9)$G65TNIh4H6nO20-*U?t4B4Gd!K4qz0;lRPyu;X9%Yfeyr(%^NWBP;>3EOFH*Z z48;tI*}1R3=%HO-Z)f_$t9i3z*r7(ghj`jI*X9k(JXxOZ5Z-859zriA%(Ev5ixmA$ z^vd;%6qdx;Y+RM&sD*tCRhsCIDWG{^e<{5y%5|Rgr|_Gk$=Q`0HW?$j9 z9RKE880D;`r|8|A|4x2O)2&u&k>h>J{!19kIpQ)uX2au2^n3br0l3o*;7(7$F9!e~ znMpLxX?a=lsx@?kF6%DMKIEOM zwFxF%fCnJIzn+d&F$yrZtl%`Ymv+!D%pQ&fn^1EitsZN`QwfeU%Cy=ph z$lajMK#l?K6HHR`4-53lj=+xDfx2+;+_Xe?F{|MF_v|mH4($GL5Y?|Agse$IFHfZ+ z_uje8ySUybX64e77;jF$Dj(yjMJa`@OALGRK<569wDN%gYzy8f*<-?`$$Z0!_3 zYp0f`zs+J|HLKx0lcVTncI*l&qF4tDFZU8{^DVD=QK5a**{+D4XLDajo&P&1+&73* z-668PkM@~5Acsgb2;)c+?Vcl)ew`85_!?tRrDwxODWTU~O_H;Q21WazCO>nG|4d>l zMXUeY_?2baJ1pl3n0lvkvgqg@lYFBh$M`$Uh@GzMxj!SpqB&cZnm zH$b=Kj1=`sooShuK`lS&umNp4dW_Kr z#H)ZIZt6lA)G8X?J2$}5i>2>`Ao(9bel$O3;Qr*v+-{>bQ0I+Hxez7 zqwy>t!W0c^I2mbosB_Fa;LNed*LzB=PBHD19xkPhf8BGkTkP)0S`i|6T5I-#3k)OI zouGSwGNZOZ3t3ul6og?oxCNp$2h+jl`GtBbWO&mq)wyo$V10#jWgcjTzTh<;-%r@` zeR7;VP{ad?LwG@rtag2YDWEMtM^VMlV~UMB2J3z*;Um0?`MKUD67~6;%2PhqxV=1c z@{+i7josZr*QIMCiOGmKO`***B(t%LEyM{4_FsQU^d-_9%J*G^qKmP?o6@4#v!e= z4|QV+K5pZMeHU5v=3^HwZ#b&o&5R@-hru}}Dvt~S&TcY=4S1m_d>4TMd}V|Y zwz8PRno&G>cF!+|q+;HtBuC})?-h<$__a!#1hsv8cUt{I31q>T42V`Kuoh*?jKk{3EAFgd-lM!;l&jhQlP zqpk<%P=1cX5zS_fnrceD#<{t(58i8ywN50jfJ}MqHGuATqWero@l%eF4UY^zUfZ71 zNV%_MA69icWYkvQY+%TvKl^>zU>Am1=BxW<=I*|LGn+0){Qx&LnU6wwg0?D0J<(=| z%09P>=w{T00Z-GyimZcfU7s``7_M7??pFVndx-~C?B&5@+(0}CY2O3n>162SB;!6w zzpz6fpF?!$Et&S1x2sg(_0IQMzqI!B)hJ(k*k{T&qo8ubCSQP&bqe=`yBq{-W7iLqo~ zW`?q4nHe%Z%*?0f-S@ek>$#rm`aRDdzdw$1UFTeUw)=fAulsesZiobxs_~_8jTlfA zxYFa&4{$1?{U9liQ+iZscX2;ouZ-Fd-=|-`SHFLqx-G`|dAH~~#5E>g0he3^mo%&K z25;&roSgL;_^&P8(2}}tj7ysGwU2>`SNe^q$Gq+|JzvTB_B!%%*}GB)`QbI-3Bz28 z-%wyd7TeV!pZAZg`}5(SwqV_do!eXNU+wl6SL~{bOHLq~w{Dqw&q{c93tNFN2{7oe zzCUSw*dxGKfn2F!O1f}^Qa1aCCyRpYhJ(d4eDK>|C1faOcJ66*Rs34MY<_&Z{|)Vy zK-(2OtZBgCV>d;40+Cvs|zms;(=a$;X!ga0)l>n$IE1}eUp0;@@oFboDMuF9$@Fsqqft{pzPC?1up z**$9;`eQtIcH;nVzUv)05U$j_p|8 zdhO*VjtijoW{dEkfzI;dokMzx@LJb21zSweBoeGHXhbx+17jmrWlXNeynQk zG-x$kt*x7=!;S8wZ|tYx%%Fx)%#U5DQXN6G^lzcrq5NqItecBm;$=cZ>xIzlDn%K< zTn~sjL(n8D2do1mT%H3n(t_dEzmIe!L;EGTE=<%?NdIk%U{UTts&Ug@mxyMce}1{2 z8S%UUXS1uBlvey!dx9QZ2A~!*j!TrER!klYHQQb=7%+IOXiu2EXKkThrF^I1$8lT& zn1>{b1p*tgh{+Lt`;9-oL~i~TX>l8v_QNb%v3;5`Kt&+%;Apdh4Clscu3_t|093~jrG$b3 z1M~HszFi5ibGU%ysXI6N-ISc&9eT-!XyKZyU%dQtWf~!~*=6Y{{gXXw%DHsjFB)v1 zG%$&W4`G|2eYpBLpGgy3b4prU>RL>)19dAH<$7s~i6_{Da|qv}vn^Op5iMY>Hb}2w z3sTFRCyW2IB`?sYQoF&-#J5Nn+f&})moXtOB(?&^O)W&maYS6 zk6-ipxIO47b#g`Ss@rcNTrVI6@@NdqVgN7}T|ow4j7f^Aidy*L$W%0%)M+`4Gwt=W zp9^4OH=Rx=R8)bC{HJxg<(Zx8%jVqw?0*aO*8%ZG&4tlRuaZ1i8sB;S7h)W}iq(`d zb0YBCh}mPRA=}wH-DTKZLR5EA+_+PG*K$zGxkKB^fB6@j&(E3T47Gd=WliOqZmNH8 zc_!>b#*z58J4HSXEC&qlGJyccvoY7%!GV?x!1=~z2gccY8QmE0wLdYg8qGT{Hr`CG zixh6k9XV}ynrGY@&6^*fP(J*s26I$bxEdaTS#*=Hy`H96)_sZ;3_R=q!gC;DOGOes z4l2)WQaosRyXDugv9pU7y+hF9YCnx(%jEy?jP5)e;676nh-(&Zu2U)=jDM0u^(?cn z9l5-cHc!<1nqbu^wKeS9Qh61@JrFVawn&-iHAl?rR z7Hf0h8i<`T1jTIrY1nIW(a53OuUe+Mw=7M5QQM317gTxZs#^Qpuk0gNW7neNZ+@Oo zwf|~x!{^x7mmg^lN;J#gMh%yp=9E_jB#0zC^=$tw6rAs#LW-(U&$JID-Ks7B__1%% zVUqiE((1WgV|Yk4)%4mw9ukguvzCf`P{C@y`!^(zU0~cVFEEOWhrK`?m|@#~`9B{N+;263hz4-Kh8jITsKCFVVsoh~3Xjq9T9` zWQA|&CKHtfWd5z>s__C!`vxfz6v;hh3i!e<%#g)&u;3%?#*uCyDNn7wrhaG3bj zrimjxT0mg&a+HF$R^b7uCWFbRuh=$ATSiXS^FHM7S$y#Ezgvpiv`i7Fhe1~yWBrJ( zeHYv6vc17>oP%J-6SUdhm=W&vA!E6A4sX!}Gy_C9}U{_X@ zghvs~zBx%SS&{O=&Z_B4w@!q7VhbmhApxe#GjDo_R-kemUxqpbAaEJ-Bn^~njCAmg z%vnk!vkvmz-GHgPF33w^-FpRu(q~qzFIG;ZJ2#lVA$%wB6M|zjM(28@(>V5@q)@7@d3XqZ9abCtM7D?=RfeIcsYJ`Rv-N;hg#Wx$lCn7(to~!3pkXQX*6K^e^RpFn1HLdh8RN zrD6dJN4KA=oD?AYEvP}O+wnDFN9LSWDp`Nzi%ofx=bMI_+Gj3+$?$+M!{XYep@Pr; z&3*NA`Kngn%%pWQb_B$N0rC!@4h3DREvl_VIrdJiyYzp@u6sQgRaZ`$RP0!$xmbH&d1FLDr1%CgU%SCq4^CFJ&jSfLeq0JXbTt`6IHsZ+# zYPdAn>yO$G7J`ovgXBGo5g!MgiHEYhXJ^_zOCT7&-_K_RD|P`ZBAcW2sv{zT|7KB* zc^~yfoG+YD^2z;IG_b zF~IU4<)%p!oGd5wdFkab#V~SSlbLuX&(G#ho>y>>S08BI@yiW7_ zp3@A`tt7@SRE5;`fA@b@^Ws7O`k(5`_$F=?YB<2Z!%?%mE}JgO35YBkwU8_M(J3hL z5{2f>9^8BXig5jirB7$(9lja-3LHX6Q1;<|Az$PD9IlxDcqZ&l&YX9UVO#vF+2`Tu zF&~$AQ3{y;puT?sR$B-H@K^v-36BPd+X+Duof{}9uCd@4wf+{8HeIse+S=K!p2SY&tb(e8X46*3gx66J?FXrQT*tPLZ<#Ry$F}b+`KEQ{=JU1*bNk;y zdO_K3PpFQlAqgffny53EzKJc{NELS*0F?%3dr6k--jU#%5YVjrY_(!k_sx{I#FNxm zH#*2!k-br;BNE@hbMJj{?QqYn+O_0b9SE5KNfx%QgfZ6B6 zWnkN4I8jQFTyx+7Pr8{Z=^q%^lGsRJ9#PcC{IazEX`Gt&TgVp9YDS6GDG=@pa&U~? zg;z`~EO%It>3I~44r&s-`^neX=8v9?mMAwu-H5f-oO{gSbx{eu9tI_ozlVVxbwR|Q zJ-46p@t_I;hJcpYgOnVaMhjI+E8*{h0NnVCCySjzbd?1UEqK&k>~I{w__y))6Atwe zw;`RFoxfOOfgTs86%-9o>64y>z6&!mDfimCSS$QjJmTq3h3^w|j9wn57DZ((y`#=8 ztiH^h_}953r(&2ep&jIsGZE;+-Cs@rq^>vPd~~1ys}K3^n8r|wuL@tF8oNiPxle({D$bz``5Amt{Bf= z{f_xF#Qy)v&-s6hi~L^z*#9}1@qf>AoayfERf>EhNfZ!4nMBtFc-E){_bS`}#nzN8 z#ec;76pa_pHXKC7DG#z-g83i@hv%gt`tdwvh>HI$l-sT`*iNNSt?&~J=}nlorM%M8 z%Q$!MbO3a3lM+PMfxb?IIpidt+VezUar<4$^!FJL3|32-1wwFB4HAc6$H;Yi⪰3;6sX?NDg%V;`Qp~C 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%0A~5G*ST{rLnS!^Xk){ekbpajU55|5{V7H8$5s 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/src/support/global-setup.ts', + globalTeardown: '/src/support/global-teardown.ts', + setupFiles: ['/src/support/test-setup.ts'], + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + }, + ], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/user-data-e2e', +}; diff --git a/apps/user-data-e2e/project.json b/apps/user-data-e2e/project.json new file mode 100644 index 0000000..840891a --- /dev/null +++ b/apps/user-data-e2e/project.json @@ -0,0 +1,22 @@ +{ + "name": "user-data-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "implicitDependencies": ["user-data"], + "targets": { + "e2e": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"], + "options": { + "jestConfig": "apps/user-data-e2e/jest.config.ts", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/user-data-e2e/**/*.{js,ts}"] + } + } + } +} diff --git a/apps/user-data-e2e/src/support/global-setup.ts b/apps/user-data-e2e/src/support/global-setup.ts new file mode 100644 index 0000000..c1f5144 --- /dev/null +++ b/apps/user-data-e2e/src/support/global-setup.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +var __TEARDOWN_MESSAGE__: string; + +module.exports = async function () { + // Start services that that the app needs to run (e.g. database, docker-compose, etc.). + console.log('\nSetting up...\n'); + + // Hint: Use `globalThis` to pass variables to global teardown. + globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'; +}; diff --git a/apps/user-data-e2e/src/support/global-teardown.ts b/apps/user-data-e2e/src/support/global-teardown.ts new file mode 100644 index 0000000..32ea345 --- /dev/null +++ b/apps/user-data-e2e/src/support/global-teardown.ts @@ -0,0 +1,7 @@ +/* eslint-disable */ + +module.exports = async function () { + // Put clean up logic here (e.g. stopping services, docker-compose, etc.). + // Hint: `globalThis` is shared between setup and teardown. + console.log(globalThis.__TEARDOWN_MESSAGE__); +}; diff --git a/apps/user-data-e2e/src/support/test-setup.ts b/apps/user-data-e2e/src/support/test-setup.ts new file mode 100644 index 0000000..07f2870 --- /dev/null +++ b/apps/user-data-e2e/src/support/test-setup.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ + +import axios from 'axios'; + +module.exports = async function () { + // Configure axios for tests to use. + const host = process.env.HOST ?? 'localhost'; + const port = process.env.PORT ?? '3000'; + axios.defaults.baseURL = `http://${host}:${port}`; +}; diff --git a/apps/user-data-e2e/src/user-data/user-data.spec.ts b/apps/user-data-e2e/src/user-data/user-data.spec.ts new file mode 100644 index 0000000..e8ac2a6 --- /dev/null +++ b/apps/user-data-e2e/src/user-data/user-data.spec.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; + +describe('GET /api', () => { + it('should return a message', async () => { + const res = await axios.get(`/api`); + + expect(res.status).toBe(200); + expect(res.data).toEqual({ message: 'Hello API' }); + }); +}); diff --git a/apps/user-data-e2e/tsconfig.json b/apps/user-data-e2e/tsconfig.json new file mode 100644 index 0000000..ed633e1 --- /dev/null +++ b/apps/user-data-e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/apps/user-data-e2e/tsconfig.spec.json b/apps/user-data-e2e/tsconfig.spec.json new file mode 100644 index 0000000..d7f9cf2 --- /dev/null +++ b/apps/user-data-e2e/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.ts"] +} diff --git a/apps/user-data/.ASC_MANIFEST b/apps/user-data/.ASC_MANIFEST new file mode 100644 index 0000000..63abe52 --- /dev/null +++ b/apps/user-data/.ASC_MANIFEST @@ -0,0 +1,13 @@ +$$ +@version: 0.1.0; +@scuuid: 27895030-15ec-11ee-be56-0242ac120002; +@type: service; +@platform: nestjs; +@license: BSD-3-Clause; +@owner: artem-darius weber; +@author: ; +@title: user-data; +@desc: ; +@rp: kubsu it lab; +@vr: 7093; +$$ diff --git a/apps/user-data/.env.example b/apps/user-data/.env.example new file mode 100644 index 0000000..6c89035 --- /dev/null +++ b/apps/user-data/.env.example @@ -0,0 +1,8 @@ +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL=postgresql://postgres:postgres@sc-user-data-database/sc-user-data +PORT=3002 diff --git a/apps/user-data/.eslintrc.json b/apps/user-data/.eslintrc.json new file mode 100644 index 0000000..9d9c0db --- /dev/null +++ b/apps/user-data/.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/apps/user-data/.gitignore b/apps/user-data/.gitignore new file mode 100644 index 0000000..11ddd8d --- /dev/null +++ b/apps/user-data/.gitignore @@ -0,0 +1,3 @@ +node_modules +# Keep environment variables out of version control +.env diff --git a/apps/user-data/Dockerfile b/apps/user-data/Dockerfile new file mode 100644 index 0000000..f0b43cf --- /dev/null +++ b/apps/user-data/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 +FROM node:18 + +WORKDIR /app + +COPY --chown=node:node . . + +RUN rm -f *.env *.env.* + +RUN apt-get update -y && apt-get install -y dumb-init + +RUN npm install + +ENV NODE_ENV=production + +RUN npm run prefullbuild && npm run prebuild && npm run build + +RUN mkdir temp temp/.prisma temp/@prisma temp/prisma && cp -r ./node_modules/.prisma/* ./temp/.prisma/ && cp -r ./node_modules/@prisma/* ./temp/@prisma/ && cp -r ./node_modules/prisma/* ./temp/prisma/ + +RUN rm -rdf node_modules + +RUN npm install --production + +RUN cp -r ./temp/* ./node_modules/ && rm -rdf temp + +RUN ls | grep -v node_modules | grep -v dist | xargs rm -rfv + +RUN cp -r ./dist/* ./ && rm -rdf dist + +USER node + +CMD ["dumb-init", "node", "./main.js"] diff --git a/apps/user-data/LICENSE b/apps/user-data/LICENSE new file mode 100644 index 0000000..d62cc9d --- /dev/null +++ b/apps/user-data/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 SC (DJEEFT) Β© + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS β€œAS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/apps/user-data/jest.config.ts b/apps/user-data/jest.config.ts new file mode 100644 index 0000000..98babab --- /dev/null +++ b/apps/user-data/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'user-data', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/apps/user-data', +}; diff --git a/apps/user-data/prisma/schema.prisma b/apps/user-data/prisma/schema.prisma new file mode 100644 index 0000000..4b6ad66 --- /dev/null +++ b/apps/user-data/prisma/schema.prisma @@ -0,0 +1,53 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model User { + uuid String @id @default(uuid()) @db.Uuid + + email Email? @relation("mainEmail") + phoneNumber PhoneNumber? @relation("mainPhoneNumber") + + reserveEmails Email[] @relation("reserveEmails") + reservePhoneNumbers PhoneNumber[] @relation("reservePhoneNumbers") + + extendedData Json @default("{}") @db.JsonB + + createdAt DateTime @db.Timestamp(3) + updatedAt DateTime @db.Timestamp(3) +} + +model Email { + uuid String @id @default(uuid()) @db.Uuid + + email String @unique @db.VarChar(256) + + userUuid String? @unique @db.Uuid + user User? @relation("mainEmail", fields: [userUuid], references: [uuid]) + + userUuidReserve String? @db.Uuid + userReserve User? @relation("reserveEmails", fields: [userUuidReserve], references: [uuid]) + + createdAt DateTime @db.Timestamp(3) + updatedAt DateTime @db.Timestamp(3) +} + +model PhoneNumber { + uuid String @id @default(uuid()) @db.Uuid + + phoneNumber String @unique @db.VarChar(20) + + userUuid String? @unique @db.Uuid + user User? @relation("mainPhoneNumber", fields: [userUuid], references: [uuid]) + + userUuidReserve String? @db.Uuid + userReserve User? @relation("reservePhoneNumbers", fields: [userUuidReserve], references: [uuid]) + + createdAt DateTime @db.Timestamp(3) + updatedAt DateTime @db.Timestamp(3) +} diff --git a/apps/user-data/project.json b/apps/user-data/project.json new file mode 100644 index 0000000..d34fe8e --- /dev/null +++ b/apps/user-data/project.json @@ -0,0 +1,64 @@ +{ + "name": "user-data", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/user-data/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@nx/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "target": "node", + "compiler": "tsc", + "outputPath": "dist/apps/user-data", + "main": "apps/user-data/src/main.ts", + "tsConfig": "apps/user-data/tsconfig.app.json", + "assets": ["apps/user-data/src/assets"], + "isolatedConfig": true, + "webpackConfig": "apps/user-data/webpack.config.js" + }, + "configurations": { + "development": {}, + "production": {} + } + }, + "serve": { + "executor": "@nx/js:node", + "defaultConfiguration": "development", + "options": { + "buildTarget": "user-data:build" + }, + "configurations": { + "development": { + "buildTarget": "user-data:build:development" + }, + "production": { + "buildTarget": "user-data:build:production" + } + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/user-data/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/user-data/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + }, + "tags": [] +} diff --git a/apps/user-data/serving/.gitignore b/apps/user-data/serving/.gitignore new file mode 100644 index 0000000..7278035 --- /dev/null +++ b/apps/user-data/serving/.gitignore @@ -0,0 +1,225 @@ +*.env +.idea + +### NotepadPP template +# Notepad++ backups # +*.bak + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Xcode template +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Gcc Patch +/*.gcno + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Kate template +# Swap Files # +.*.kate-swp +.swp.* + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### SublimeText template +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json +sftp-config-alt*.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings diff --git a/apps/user-data/serving/database.env.example b/apps/user-data/serving/database.env.example new file mode 100644 index 0000000..c44f3ae --- /dev/null +++ b/apps/user-data/serving/database.env.example @@ -0,0 +1,3 @@ +POSTGRES_DB=sc-user-data +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres diff --git a/apps/user-data/serving/docker-compose.yaml b/apps/user-data/serving/docker-compose.yaml new file mode 100644 index 0000000..0e8a651 --- /dev/null +++ b/apps/user-data/serving/docker-compose.yaml @@ -0,0 +1,34 @@ +version: "3.9" + +services: + user-data-database: + image: postgres:latest + container_name: user-data-database + restart: always + networks: + - user-data + volumes: + - user-data-database:/var/lib/postgresql + env_file: + - ./database.env + user-data-database-admin: + container_name: user-data-database-admin + image: bitnami/phppgadmin:latest + restart: always + networks: + - user-data + depends_on: + - user-data-database + environment: + - DATABASE_HOST=user-data-database + ports: + - "8083:8080" + +networks: + user-data: + name: user-data + driver: bridge + +volumes: + user-data-database: + driver: local diff --git a/apps/user-data/src/app/app.controller.spec.ts b/apps/user-data/src/app/app.controller.spec.ts new file mode 100644 index 0000000..a6e8d35 --- /dev/null +++ b/apps/user-data/src/app/app.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { AppController } from './app.controller'; +import { AppService } from './service/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-data/src/app/app.controller.ts b/apps/user-data/src/app/app.controller.ts new file mode 100644 index 0000000..0c6a72d --- /dev/null +++ b/apps/user-data/src/app/app.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; + +import { AppService } from './service/app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getData() { + return this.appService.getData(); + } +} diff --git a/apps/user-data/src/app/app.module.ts b/apps/user-data/src/app/app.module.ts new file mode 100644 index 0000000..e8f09b7 --- /dev/null +++ b/apps/user-data/src/app/app.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { AppController } from './app.controller'; +import { AppService } from './service/app.service'; +import { UserModule } from "../user/user.module"; +import { EmailModule } from "../email/email.module"; +import { PhoneNumberModule } from "../phone-number/phone-number.module"; + +@Module({ + imports: [UserModule, EmailModule, PhoneNumberModule], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} diff --git a/apps/user-data/src/app/service/app.service.spec.ts b/apps/user-data/src/app/service/app.service.spec.ts new file mode 100644 index 0000000..42cf0a2 --- /dev/null +++ b/apps/user-data/src/app/service/app.service.spec.ts @@ -0,0 +1,21 @@ +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-data/src/app/service/app.service.ts b/apps/user-data/src/app/service/app.service.ts new file mode 100644 index 0000000..cd8cede --- /dev/null +++ b/apps/user-data/src/app/service/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getData(): { message: string } { + return { message: 'Hello API' }; + } +} diff --git a/apps/user-data/src/assets/.gitkeep b/apps/user-data/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/user-data/src/email/email.controller.spec.ts b/apps/user-data/src/email/email.controller.spec.ts new file mode 100644 index 0000000..9470aa8 --- /dev/null +++ b/apps/user-data/src/email/email.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EmailController } from './email.controller'; + +describe('EmailController', () => { + let controller: EmailController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [EmailController], + }).compile(); + + controller = module.get(EmailController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/user-data/src/email/email.controller.ts b/apps/user-data/src/email/email.controller.ts new file mode 100644 index 0000000..f4dade4 --- /dev/null +++ b/apps/user-data/src/email/email.controller.ts @@ -0,0 +1,53 @@ +import { Body, Controller, HttpCode, Inject, Post, UseInterceptors } from "@nestjs/common"; +import { Prisma, Email } from "@prisma/client"; +import { EmailService } from "./service/email.service"; +import { PrismaErrorInterceptor } from "../prisma/interceptor/prisma-error.interceptor"; + +@Controller("email") +@UseInterceptors(PrismaErrorInterceptor) +export class EmailController { + constructor(@Inject(EmailService) private email: EmailService) { + } + + @Post("find/unique") + @HttpCode(200) + async FindUnique(@Body() emailFindUniqueArgs: Prisma.EmailFindUniqueArgs): Promise | null> { + return await this.email.FindUnique(emailFindUniqueArgs); + } + + @Post("find/first") + @HttpCode(200) + async FindFirst(@Body() emailFindFirstArgs: Prisma.EmailFindFirstArgs): Promise | null> { + return await this.email.FindFirst(emailFindFirstArgs); + } + + @Post("find/many") + @HttpCode(200) + async FindMany(@Body() emailFindManyArgs: Prisma.EmailFindManyArgs): Promise[]> { + return await this.email.FindMany(emailFindManyArgs); + } + + @Post("update") + @HttpCode(200) + async Update(@Body() emailUpdateArgs: Prisma.EmailUpdateArgs): Promise> { + return await this.email.Update(emailUpdateArgs); + } + + @Post("count") + @HttpCode(200) + async Count(@Body() emailCountArgs: Prisma.EmailCountArgs): Promise { + return await this.email.Count(emailCountArgs); + } + + @Post("create") + @HttpCode(200) + async Create(@Body() emailCreateArgs: Prisma.EmailCreateArgs): Promise> { + return await this.email.Create(emailCreateArgs); + } + + @Post("delete") + @HttpCode(200) + async Delete(@Body() emailDeleteArgs: Prisma.EmailDeleteArgs): Promise> { + return await this.email.Delete(emailDeleteArgs); + } +} diff --git a/apps/user-data/src/email/email.module.ts b/apps/user-data/src/email/email.module.ts new file mode 100644 index 0000000..693fee6 --- /dev/null +++ b/apps/user-data/src/email/email.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { EmailController } from "./email.controller"; +import { EmailService } from "./service/email.service"; +import { PrismaModule } from "../prisma/prisma.module"; + +@Module({ + imports: [PrismaModule], + controllers: [EmailController], + providers: [EmailService] +}) +export class EmailModule { +} diff --git a/apps/user-data/src/email/service/email.service.spec.ts b/apps/user-data/src/email/service/email.service.spec.ts new file mode 100644 index 0000000..27719da --- /dev/null +++ b/apps/user-data/src/email/service/email.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EmailService } from './email.service'; + +describe('EmailService', () => { + let service: EmailService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EmailService], + }).compile(); + + service = module.get(EmailService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/user-data/src/email/service/email.service.ts b/apps/user-data/src/email/service/email.service.ts new file mode 100644 index 0000000..b64799c --- /dev/null +++ b/apps/user-data/src/email/service/email.service.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { PrismaService } from '../../prisma/service/prisma.service'; +import { Email, Prisma } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import { UniqueConstraintFailedError } from "../../prisma/error/unique-constraint-failed.error"; +import { RecordNotFoundError } from "../../prisma/error/record-not-found.error"; + +@Injectable() +export class EmailService { + constructor(@Inject(PrismaService) private prisma: PrismaService) { + } + + async FindUnique(emailFindUniqueArgs: Prisma.EmailFindUniqueArgs): Promise | null> { + return await this.prisma.email.findUnique(emailFindUniqueArgs); + } + + async FindFirst(emailFindFirstArgs: Prisma.EmailFindFirstArgs): Promise | null> { + return await this.prisma.email.findFirst(emailFindFirstArgs); + } + + async FindMany(emailFindManyArgs: Prisma.EmailFindManyArgs): Promise[]> { + return await this.prisma.email.findMany(emailFindManyArgs); + } + + async Update(emailUpdateArgs: Prisma.EmailUpdateArgs): Promise> { + try { + return await this.prisma.email.update(emailUpdateArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") throw new RecordNotFoundError("Record to update not found."); + if (e.code === "P2002") throw new UniqueConstraintFailedError("Unique constant failed"); + } + throw e; + } + } + + async Count(emailCountArgs: Prisma.EmailCountArgs): Promise { + return await this.prisma.email.count(emailCountArgs); + } + + async Create(emailCreateArgs: Prisma.EmailCreateArgs): Promise> { + try { + return await this.prisma.email.create(emailCreateArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2002") throw new UniqueConstraintFailedError("Unique constant failed"); + } + throw e; + } + } + + async Delete(emailDeleteArgs: Prisma.EmailDeleteArgs): Promise> { + try { + return await this.prisma.email.delete(emailDeleteArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") throw new RecordNotFoundError("Record to delete not found."); + } + throw e; + } + } +} diff --git a/apps/user-data/src/main.ts b/apps/user-data/src/main.ts new file mode 100644 index 0000000..5794fe8 --- /dev/null +++ b/apps/user-data/src/main.ts @@ -0,0 +1,22 @@ +/** + * This is not a production server yet! + * This is only a minimal backend to get started. + */ + +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; + +import { AppModule } from './app/app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const globalPrefix = 'api'; + app.setGlobalPrefix(globalPrefix); + const port = process.env.PORT || 3002; + await app.listen(port); + Logger.log( + `πŸš€ User Data Application is running on: http://localhost:${port}/${globalPrefix}` + ); +} + +bootstrap(); diff --git a/apps/user-data/src/phone-number/phone-number.controller.spec.ts b/apps/user-data/src/phone-number/phone-number.controller.spec.ts new file mode 100644 index 0000000..ed89b8a --- /dev/null +++ b/apps/user-data/src/phone-number/phone-number.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PhoneNumberController } from './phone-number.controller'; + +describe('PhoneNumberController', () => { + let controller: PhoneNumberController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [PhoneNumberController], + }).compile(); + + controller = module.get(PhoneNumberController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/user-data/src/phone-number/phone-number.controller.ts b/apps/user-data/src/phone-number/phone-number.controller.ts new file mode 100644 index 0000000..a4fae62 --- /dev/null +++ b/apps/user-data/src/phone-number/phone-number.controller.ts @@ -0,0 +1,53 @@ +import { Body, Controller, HttpCode, Inject, Post, UseInterceptors } from "@nestjs/common"; +import { PhoneNumber, Prisma } from "@prisma/client"; +import { PhoneNumberService } from "./service/phone-number.service"; +import { PrismaErrorInterceptor } from "../prisma/interceptor/prisma-error.interceptor"; + +@Controller("phoneNumber") +@UseInterceptors(PrismaErrorInterceptor) +export class PhoneNumberController { + constructor(@Inject(PhoneNumberService) private phoneNumber: PhoneNumberService) { + } + + @Post("find/unique") + @HttpCode(200) + async FindUnique(@Body() phoneNumberFindUniqueArgs: Prisma.PhoneNumberFindUniqueArgs): Promise | null> { + return await this.phoneNumber.FindUnique(phoneNumberFindUniqueArgs); + } + + @Post("find/first") + @HttpCode(200) + async FindFirst(@Body() phoneNumberFindFirstArgs: Prisma.PhoneNumberFindFirstArgs): Promise | null> { + return await this.phoneNumber.FindFirst(phoneNumberFindFirstArgs); + } + + @Post("find/many") + @HttpCode(200) + async FindMany(@Body() phoneNumberFindManyArgs: Prisma.PhoneNumberFindManyArgs): Promise[]> { + return await this.phoneNumber.FindMany(phoneNumberFindManyArgs); + } + + @Post("update") + @HttpCode(200) + async Update(@Body() phoneNumberUpdateArgs: Prisma.PhoneNumberUpdateArgs): Promise> { + return await this.phoneNumber.Update(phoneNumberUpdateArgs); + } + + @Post("count") + @HttpCode(200) + async Count(@Body() phoneNumberCountArgs: Prisma.PhoneNumberCountArgs): Promise { + return await this.phoneNumber.Count(phoneNumberCountArgs); + } + + @Post("create") + @HttpCode(200) + async Create(@Body() phoneNumberCreateArgs: Prisma.PhoneNumberCreateArgs): Promise> { + return await this.phoneNumber.Create(phoneNumberCreateArgs); + } + + @Post("delete") + @HttpCode(200) + async Delete(@Body() phoneNumberDeleteArgs: Prisma.PhoneNumberDeleteArgs): Promise> { + return await this.phoneNumber.Delete(phoneNumberDeleteArgs); + } +} diff --git a/apps/user-data/src/phone-number/phone-number.module.ts b/apps/user-data/src/phone-number/phone-number.module.ts new file mode 100644 index 0000000..4a8e411 --- /dev/null +++ b/apps/user-data/src/phone-number/phone-number.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { PhoneNumberController } from "./phone-number.controller"; +import { PhoneNumberService } from "./service/phone-number.service"; +import { PrismaModule } from "../prisma/prisma.module"; + +@Module({ + imports: [PrismaModule], + controllers: [PhoneNumberController], + providers: [PhoneNumberService] +}) +export class PhoneNumberModule { +} diff --git a/apps/user-data/src/phone-number/service/phone-number.service.spec.ts b/apps/user-data/src/phone-number/service/phone-number.service.spec.ts new file mode 100644 index 0000000..9b49efb --- /dev/null +++ b/apps/user-data/src/phone-number/service/phone-number.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PhoneNumberService } from './phone-number.service'; + +describe('PhoneNumberService', () => { + let service: PhoneNumberService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PhoneNumberService], + }).compile(); + + service = module.get(PhoneNumberService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/user-data/src/phone-number/service/phone-number.service.ts b/apps/user-data/src/phone-number/service/phone-number.service.ts new file mode 100644 index 0000000..150bf3d --- /dev/null +++ b/apps/user-data/src/phone-number/service/phone-number.service.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { PrismaService } from "../../prisma/service/prisma.service"; +import { PhoneNumber, Prisma } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import { UniqueConstraintFailedError } from "../../prisma/error/unique-constraint-failed.error"; +import { RecordNotFoundError } from "../../prisma/error/record-not-found.error"; + +@Injectable() +export class PhoneNumberService { + constructor(@Inject(PrismaService) private prisma: PrismaService) { + } + + async FindUnique(phoneNumberFindUniqueArgs: Prisma.PhoneNumberFindUniqueArgs): Promise | null> { + return await this.prisma.phoneNumber.findUnique(phoneNumberFindUniqueArgs); + } + + async FindFirst(phoneNumberFindFirstArgs: Prisma.PhoneNumberFindFirstArgs): Promise | null> { + return await this.prisma.phoneNumber.findFirst(phoneNumberFindFirstArgs); + } + + async FindMany(phoneNumberFindManyArgs: Prisma.PhoneNumberFindManyArgs): Promise[]> { + return await this.prisma.phoneNumber.findMany(phoneNumberFindManyArgs); + } + + async Update(phoneNumberUpdateArgs: Prisma.PhoneNumberUpdateArgs): Promise> { + try { + return await this.prisma.phoneNumber.update(phoneNumberUpdateArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") throw new RecordNotFoundError("Record to update not found."); + if (e.code === "P2002") throw new UniqueConstraintFailedError("Unique constant failed"); + } + throw e; + } + } + + async Count(phoneNumberCountArgs: Prisma.PhoneNumberCountArgs): Promise { + return await this.prisma.phoneNumber.count(phoneNumberCountArgs); + } + + async Create(phoneNumberCreateArgs: Prisma.PhoneNumberCreateArgs): Promise> { + try { + return await this.prisma.phoneNumber.create(phoneNumberCreateArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2002") throw new UniqueConstraintFailedError("Unique constant failed"); + } + throw e; + } + } + + async Delete(phoneNumberDeleteArgs: Prisma.PhoneNumberDeleteArgs): Promise> { + try { + return await this.prisma.phoneNumber.delete(phoneNumberDeleteArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") throw new RecordNotFoundError("Record to delete not found."); + } + throw e; + } + } +} diff --git a/apps/user-data/src/prisma/error/record-not-found.error.ts b/apps/user-data/src/prisma/error/record-not-found.error.ts new file mode 100644 index 0000000..e1c9cb1 --- /dev/null +++ b/apps/user-data/src/prisma/error/record-not-found.error.ts @@ -0,0 +1 @@ +export class RecordNotFoundError extends Error {} diff --git a/apps/user-data/src/prisma/error/unique-constraint-failed.error.ts b/apps/user-data/src/prisma/error/unique-constraint-failed.error.ts new file mode 100644 index 0000000..647b582 --- /dev/null +++ b/apps/user-data/src/prisma/error/unique-constraint-failed.error.ts @@ -0,0 +1 @@ +export class UniqueConstraintFailedError extends Error {} diff --git a/apps/user-data/src/prisma/interceptor/prisma-error.interceptor.ts b/apps/user-data/src/prisma/interceptor/prisma-error.interceptor.ts new file mode 100644 index 0000000..7ad4b9a --- /dev/null +++ b/apps/user-data/src/prisma/interceptor/prisma-error.interceptor.ts @@ -0,0 +1,26 @@ +import { + CallHandler, + ConflictException, + ExecutionContext, + Injectable, + NestInterceptor, + NotFoundException +} from "@nestjs/common"; +import { catchError, Observable } from "rxjs"; +import { RecordNotFoundError } from "../error/record-not-found.error"; +import { UniqueConstraintFailedError } from "../error/unique-constraint-failed.error"; + +@Injectable() +export class PrismaErrorInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next + .handle() + .pipe( + catchError(err => { + if (err instanceof RecordNotFoundError) throw new NotFoundException(err.message); + if (err instanceof UniqueConstraintFailedError) throw new ConflictException(err.message); + throw err; + }) + ); + } +} diff --git a/apps/user-data/src/prisma/prisma.module.ts b/apps/user-data/src/prisma/prisma.module.ts new file mode 100644 index 0000000..44400fd --- /dev/null +++ b/apps/user-data/src/prisma/prisma.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from "@nestjs/common"; +import { PrismaService } from "./service/prisma.service"; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService] +}) +export class PrismaModule { +} diff --git a/apps/user-data/src/prisma/service/prisma.service.spec.ts b/apps/user-data/src/prisma/service/prisma.service.spec.ts new file mode 100644 index 0000000..a68cb9e --- /dev/null +++ b/apps/user-data/src/prisma/service/prisma.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from './prisma.service'; + +describe('PrismaService', () => { + let service: PrismaService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PrismaService], + }).compile(); + + service = module.get(PrismaService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/user-data/src/prisma/service/prisma.service.ts b/apps/user-data/src/prisma/service/prisma.service.ts new file mode 100644 index 0000000..aaa764a --- /dev/null +++ b/apps/user-data/src/prisma/service/prisma.service.ts @@ -0,0 +1,15 @@ +import { INestApplication, Injectable, OnModuleInit } from "@nestjs/common"; +import { PrismaClient } from "@prisma/client"; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on("beforeExit", async () => { + await app.close(); + }); + } +} diff --git a/apps/user-data/src/user/service/user.service.spec.ts b/apps/user-data/src/user/service/user.service.spec.ts new file mode 100644 index 0000000..873de8a --- /dev/null +++ b/apps/user-data/src/user/service/user.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserService], + }).compile(); + + service = module.get(UserService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/user-data/src/user/service/user.service.ts b/apps/user-data/src/user/service/user.service.ts new file mode 100644 index 0000000..413d64a --- /dev/null +++ b/apps/user-data/src/user/service/user.service.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { PrismaService } from "../../prisma/service/prisma.service"; +import { Prisma, User } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import { UniqueConstraintFailedError } from "../../prisma/error/unique-constraint-failed.error"; +import { RecordNotFoundError } from "../../prisma/error/record-not-found.error"; + +@Injectable() +export class UserService { + constructor(@Inject(PrismaService) private prisma: PrismaService) { + } + + async FindUnique(userFindUniqueArgs: Prisma.UserFindUniqueArgs): Promise | null> { + return await this.prisma.user.findUnique(userFindUniqueArgs); + } + + async FindFirst(userFindFirstArgs: Prisma.UserFindFirstArgs): Promise | null> { + return await this.prisma.user.findFirst(userFindFirstArgs); + } + + async FindMany(userFindManyArgs: Prisma.UserFindManyArgs): Promise[]> { + return await this.prisma.user.findMany(userFindManyArgs); + } + + async Update(userUpdateArgs: Prisma.UserUpdateArgs): Promise> { + try { + return await this.prisma.user.update(userUpdateArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") throw new RecordNotFoundError("Record to update not found."); + if (e.code === "P2002") throw new UniqueConstraintFailedError("Unique constant failed"); + } + throw e; + } + } + + async Count(userCountArgs: Prisma.UserCountArgs): Promise { + return await this.prisma.user.count(userCountArgs); + } + + async Create(userCreateArgs: Prisma.UserCreateArgs): Promise> { + try { + return await this.prisma.user.create(userCreateArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2002") throw new UniqueConstraintFailedError("Unique constant failed"); + } + throw e; + } + } + + async Delete(userDeleteArgs: Prisma.UserDeleteArgs): Promise> { + try { + return await this.prisma.user.delete(userDeleteArgs); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") throw new RecordNotFoundError("Record to delete not found."); + } + throw e; + } + } +} diff --git a/apps/user-data/src/user/user.controller.spec.ts b/apps/user-data/src/user/user.controller.spec.ts new file mode 100644 index 0000000..7057a1a --- /dev/null +++ b/apps/user-data/src/user/user.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserController } from './user.controller'; + +describe('UserController', () => { + let controller: UserController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + }).compile(); + + controller = module.get(UserController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/user-data/src/user/user.controller.ts b/apps/user-data/src/user/user.controller.ts new file mode 100644 index 0000000..66dbd28 --- /dev/null +++ b/apps/user-data/src/user/user.controller.ts @@ -0,0 +1,53 @@ +import { Body, Controller, HttpCode, Inject, Post, UseInterceptors } from "@nestjs/common"; +import { Prisma, User } from "@prisma/client"; +import { UserService } from "./service/user.service"; +import { PrismaErrorInterceptor } from "../prisma/interceptor/prisma-error.interceptor"; + +@Controller("user") +@UseInterceptors(PrismaErrorInterceptor) +export class UserController { + constructor(@Inject(UserService) private user: UserService) { + } + + @Post("find/unique") + @HttpCode(200) + async FindUnique(@Body() userFindUniqueArgs: Prisma.UserFindUniqueArgs): Promise | null> { + return await this.user.FindUnique(userFindUniqueArgs); + } + + @Post("find/first") + @HttpCode(200) + async FindFirst(@Body() userFindFirstArgs: Prisma.UserFindFirstArgs): Promise | null> { + return await this.user.FindFirst(userFindFirstArgs); + } + + @Post("find/many") + @HttpCode(200) + async FindMany(@Body() userFindManyArgs: Prisma.UserFindManyArgs): Promise[]> { + return await this.user.FindMany(userFindManyArgs); + } + + @Post("update") + @HttpCode(200) + async Update(@Body() userUpdateArgs: Prisma.UserUpdateArgs): Promise> { + return await this.user.Update(userUpdateArgs); + } + + @Post("count") + @HttpCode(200) + async Count(@Body() userCountArgs: Prisma.UserCountArgs): Promise { + return await this.user.Count(userCountArgs); + } + + @Post("create") + @HttpCode(200) + async Create(@Body() userCreateArgs: Prisma.UserCreateArgs): Promise> { + return await this.user.Create(userCreateArgs); + } + + @Post("delete") + @HttpCode(200) + async Delete(@Body() userDeleteArgs: Prisma.UserDeleteArgs): Promise> { + return await this.user.Delete(userDeleteArgs); + } +} diff --git a/apps/user-data/src/user/user.module.ts b/apps/user-data/src/user/user.module.ts new file mode 100644 index 0000000..250f120 --- /dev/null +++ b/apps/user-data/src/user/user.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { UserController } from "./user.controller"; +import { UserService } from "./service/user.service"; +import { PrismaModule } from "../prisma/prisma.module"; + +@Module({ + imports: [PrismaModule], + controllers: [UserController], + providers: [UserService] +}) +export class UserModule { +} diff --git a/apps/user-data/tsconfig.app.json b/apps/user-data/tsconfig.app.json new file mode 100644 index 0000000..a2ce765 --- /dev/null +++ b/apps/user-data/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["node"], + "emitDecoratorMetadata": true, + "target": "es2021" + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/apps/user-data/tsconfig.json b/apps/user-data/tsconfig.json new file mode 100644 index 0000000..8fe9526 --- /dev/null +++ b/apps/user-data/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "esModuleInterop": true + }, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "es2017", + "sourceMap": true, + "incremental": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true +} diff --git a/apps/user-data/tsconfig.spec.json b/apps/user-data/tsconfig.spec.json new file mode 100644 index 0000000..9b2a121 --- /dev/null +++ b/apps/user-data/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/apps/user-data/webpack.config.js b/apps/user-data/webpack.config.js new file mode 100644 index 0000000..81db92b --- /dev/null +++ b/apps/user-data/webpack.config.js @@ -0,0 +1,8 @@ +const { composePlugins, withNx } = require('@nx/webpack'); + +// Nx plugins for webpack. +module.exports = composePlugins(withNx(), (config) => { + // Update the webpack config as needed here. + // e.g. `config.plugins.push(new MyPlugin())` + return config; +}); diff --git a/docker-compose.yml b/docker-compose.yml index e69de29..cc4fc41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3' +services: + zookeeper-log: + image: confluentinc/cp-zookeeper:latest + container_name: zookeeper + ports: + - "2181:2181" + environment: + - ZOOKEEPER_CLIENT_PORT=2181 + - ZOOKEEPER_TICK_TIME=2000 + networks: + - dev-panels + - kafka-log + + kafka-log: + image: confluentinc/cp-kafka:latest + container_name: kafka + ports: + - "9092:9092" + environment: + - KAFKA_BROKER_ID=1 + - KAFKA_LISTENERS=PLAINTEXT://:9092 + - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 + - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 + - KAFKA_ZOOKEEPER_CONNECT=zookeeper-log:2181 + depends_on: + - zookeeper-log + networks: + - kafka-log + + kafka-log-manager: + image: hlebalbau/kafka-manager:latest + container_name: kafka-manager + ports: + - "9000:9000" + environment: + - ZK_HOSTS=zookeeper:2181 + depends_on: + - zookeeper-log + networks: + - kafka-log + +networks: + dev-panels: + driver: bridge + kafka-log: + driver: bridge diff --git a/package-lock.json b/package-lock.json index fa8b283..e705bc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/graphql": "^12.0.3", "@nestjs/platform-express": "^10.0.2", "@nestjs/typeorm": "^10.0.0", + "@prisma/client": "^4.16.1", "@swc/helpers": "~0.5.0", "axios": "^1.0.0", "graphql": "^16.7.1", @@ -66,6 +67,7 @@ "nx": "16.4.0", "nx-cloud": "latest", "prettier": "^2.6.2", + "prisma": "^4.16.1", "react-refresh": "^0.10.0", "ts-jest": "^29.1.0", "ts-node": "10.9.1", @@ -4469,6 +4471,38 @@ "node": ">= 8" } }, + "node_modules/@prisma/client": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.1.tgz", + "integrity": "sha512-CoDHu7Bt+NuDo40ijoeHP79EHtECsPBTy3yte5Yo3op8TqXt/kV0OT5OrsWewKvQGKFMHhYQ+ePed3zzjYdGAw==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.1.tgz", + "integrity": "sha512-gpZG0kGGxfemgvK/LghHdBIz+crHkZjzszja94xp4oytpsXrgt/Ice82MvPsWMleVIniKuARrowtsIsim0PFJQ==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c.tgz", + "integrity": "sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA==" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -15387,6 +15421,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.1.tgz", + "integrity": "sha512-C2Xm7yxHxjFjjscBEW4tmoraPHH/Vyu/A0XABdbaFtoiOZARsxvOM7rwc2iZ0qVxbh0bGBGBWZUSXO/52/nHBQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.16.1" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index 44ff2ca..176c8cb 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@nestjs/graphql": "^12.0.3", "@nestjs/platform-express": "^10.0.2", "@nestjs/typeorm": "^10.0.0", + "@prisma/client": "^4.16.1", "@swc/helpers": "~0.5.0", "axios": "^1.0.0", "graphql": "^16.7.1", @@ -62,6 +63,7 @@ "nx": "16.4.0", "nx-cloud": "latest", "prettier": "^2.6.2", + "prisma": "^4.16.1", "react-refresh": "^0.10.0", "ts-jest": "^29.1.0", "ts-node": "10.9.1", From bf39c11417abd807b3587f8b12a8346263ac9b62 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Wed, 28 Jun 2023 23:04:13 +0300 Subject: [PATCH 02/17] docs: update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c16a4d2..ffa8ead 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ ## БСрвисы - [frontend (ReactJS)](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/frontend) - [AGW](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/agw) +- [User Data Service](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/user-data) ## Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅ @@ -47,6 +48,9 @@ graph TD; agw-e2w-->agw; frontend-e2e-->frontend; frontend-->agw; + agw-->user-data; + user-data-->user-data-db; + user-data-db-->user-data-pg-admin; ``` ## Generate code From 6105c04b83238ad20e12b919e2060b14789d1758 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Wed, 28 Jun 2023 23:07:44 +0300 Subject: [PATCH 03/17] docs: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ffa8ead..eaaab6b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ To start the development server run `nx serve frontend`. Open your browser and n Run all services: ``` -nx run-many --parallel --target=serve --projects=agw,frontend +nx run-many --parallel --target=serve --projects=agw,user-data,frontend ``` Deploy on SCOS From c08f644a5e0772a6cc752510ef43a8d13a452f36 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 15:41:19 +0300 Subject: [PATCH 04/17] docs: update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eaaab6b..1233672 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [frontend (ReactJS)](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/frontend) - [AGW](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/agw) - [User Data Service](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/user-data) +- [Auth Service](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/auth) ## Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅ @@ -29,7 +30,7 @@ To start the development server run `nx serve frontend`. Open your browser and n Run all services: ``` -nx run-many --parallel --target=serve --projects=agw,user-data,frontend +nx run-many --parallel --target=serve --projects=agw,user-data,auth,frontend ``` Deploy on SCOS @@ -51,6 +52,7 @@ graph TD; agw-->user-data; user-data-->user-data-db; user-data-db-->user-data-pg-admin; + agw-->auth; ``` ## Generate code From a304e567436cb27d34100763d308f8f8ce74ce73 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:06:35 +0300 Subject: [PATCH 05/17] require: @nestjs/config @nestjs/jwt class-transformer class-validator --- package-lock.json | 151 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 4 ++ 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e705bc4..d4bd170 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,17 @@ "@apollo/server": "^4.7.5", "@nestjs/apollo": "^12.0.3", "@nestjs/common": "^10.0.2", + "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.2", "@nestjs/graphql": "^12.0.3", + "@nestjs/jwt": "^10.1.0", "@nestjs/platform-express": "^10.0.2", "@nestjs/typeorm": "^10.0.0", "@prisma/client": "^4.16.1", "@swc/helpers": "~0.5.0", "axios": "^1.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "graphql": "^16.7.1", "react": "18.2.0", "react-dom": "18.2.0", @@ -3241,6 +3245,40 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.0.0.tgz", + "integrity": "sha512-fzASk1Uv6AjdE6uA1na8zpqRCXAhRpcfgpCVv3SAKlgJ3VR3bEjcI4G17WHLgLBsmPzI1ofdkSI451WLD1F1Rw==", + "dependencies": { + "dotenv": "16.1.4", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.1.4.tgz", + "integrity": "sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/@nestjs/config/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/core": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.0.3.tgz", @@ -3398,6 +3436,18 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@nestjs/jwt": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.1.0.tgz", + "integrity": "sha512-iLwCGS25ybUxGS7i5j/Mwuyzvp/WxJftHlm8aLEBv5GV92apz6L1QVjxLdZrqXbzo++C8gdJauhzil8qitY+6w==", + "dependencies": { + "@types/jsonwebtoken": "9.0.2", + "jsonwebtoken": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", @@ -5468,6 +5518,14 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -5650,6 +5708,11 @@ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.7.17", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", + "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" + }, "node_modules/@types/ws": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", @@ -7305,6 +7368,11 @@ "node": "*" } }, + "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", @@ -7550,6 +7618,21 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "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.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "dependencies": { + "@types/validator": "^13.7.10", + "libphonenumber-js": "^1.10.14", + "validator": "^13.7.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -8983,6 +9066,14 @@ "node": ">=10" } }, + "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/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -8999,6 +9090,14 @@ "safer-buffer": "^2.1.0" } }, + "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", @@ -13077,6 +13176,21 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -13105,6 +13219,25 @@ "node": ">=4.0" } }, + "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/keyv": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", @@ -13268,6 +13401,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.37", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.37.tgz", + "integrity": "sha512-Z10PCaOCiAxbUxLyR31DNeeNugSVP6iv/m7UrSKS5JHziEMApJtgku4e9Q69pzzSC9LnQiM09sqsGf2ticZnMw==" + }, "node_modules/license-webpack-plugin": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", @@ -16185,7 +16323,6 @@ "version": "7.5.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -16227,7 +16364,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -16238,8 +16374,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -18261,6 +18396,14 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/validator": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", + "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-or-promise": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", diff --git a/package.json b/package.json index 176c8cb..e51a9aa 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,17 @@ "@apollo/server": "^4.7.5", "@nestjs/apollo": "^12.0.3", "@nestjs/common": "^10.0.2", + "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.2", "@nestjs/graphql": "^12.0.3", + "@nestjs/jwt": "^10.1.0", "@nestjs/platform-express": "^10.0.2", "@nestjs/typeorm": "^10.0.0", "@prisma/client": "^4.16.1", "@swc/helpers": "~0.5.0", "axios": "^1.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "graphql": "^16.7.1", "react": "18.2.0", "react-dom": "18.2.0", From 3bd443daf7439c8d4c0b8643d29c4f7abe7c67a9 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:07:10 +0300 Subject: [PATCH 06/17] docs: add readme for user data --- apps/user-data/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/user-data/README.md diff --git a/apps/user-data/README.md b/apps/user-data/README.md new file mode 100644 index 0000000..7b3eee9 --- /dev/null +++ b/apps/user-data/README.md @@ -0,0 +1,15 @@ +
+ +

IT Lab Platform - User Data Service

+
+ +
+ ✨ + + Π­Ρ‚Π° дирСктория содСрТит исходный ΠΊΠΎΠ΄ сСрвиса Π±Π°Π·ΠΎΠ²Ρ‹Ρ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΡ… Π΄Π°Π½Π½Ρ‹Ρ… прилоТСния "IT Lab Application". + +
+ +## Ports +- `8083` - pg Admin +- `3002` - app From 34af85c04ebec85b2e035ae9fafe09327b892ded Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:08:30 +0300 Subject: [PATCH 07/17] feat: auth service init --- apps/auth/.ASC_MANIFEST | 13 + apps/auth/.env.example | 6 + apps/auth/.gitignore | 335 ++++++++++++++++++ apps/auth/Dockerfile | 32 ++ apps/auth/LICENSE | 11 + apps/auth/README.md | 32 ++ apps/auth/prisma/.gitignore | 1 + apps/auth/serving/.gitignore | 225 ++++++++++++ apps/auth/serving/database.env.example | 3 + apps/auth/serving/docker-compose.yaml | 36 ++ apps/auth/src/main.ts | 2 +- .../.gitkeep => tokens.example/jwt.key} | 0 apps/auth/tokens.example/jwt.key.pub | 0 apps/user-data/prisma/.gitignore | 1 + apps/user-data/serving/.gitignore | 2 +- 15 files changed, 697 insertions(+), 2 deletions(-) create mode 100644 apps/auth/.ASC_MANIFEST create mode 100644 apps/auth/.env.example create mode 100644 apps/auth/.gitignore create mode 100644 apps/auth/Dockerfile create mode 100644 apps/auth/LICENSE create mode 100644 apps/auth/README.md create mode 100644 apps/auth/prisma/.gitignore create mode 100644 apps/auth/serving/.gitignore create mode 100644 apps/auth/serving/database.env.example create mode 100644 apps/auth/serving/docker-compose.yaml rename apps/auth/{src/assets/.gitkeep => tokens.example/jwt.key} (100%) create mode 100644 apps/auth/tokens.example/jwt.key.pub create mode 100644 apps/user-data/prisma/.gitignore diff --git a/apps/auth/.ASC_MANIFEST b/apps/auth/.ASC_MANIFEST new file mode 100644 index 0000000..0f5345b --- /dev/null +++ b/apps/auth/.ASC_MANIFEST @@ -0,0 +1,13 @@ +$$ +@version: 0.1.0; +@scuuid: fbc45229-30c1-4daf-8f28-063bf27ef7d3; +@type: service; +@platform: nestjs; +@license: BSD-3-Clause; +@owner: artem-darius weber; +@author: ; +@title: user-authentication; +@desc: ; +@rp: kubsu it lab; +@vr: 7093; +$$ diff --git a/apps/auth/.env.example b/apps/auth/.env.example new file mode 100644 index 0000000..fc84cb9 --- /dev/null +++ b/apps/auth/.env.example @@ -0,0 +1,6 @@ +PORT=3005 + +DATABASE_URL=postgresql://postgres:postgres@localhost:5433/user-authentication + +PUBLIC_KEY_PATH=tokens/jwt.key.pub +PRIVATE_KEY_PATH=tokens/jwt.key diff --git a/apps/auth/.gitignore b/apps/auth/.gitignore new file mode 100644 index 0000000..928a9a9 --- /dev/null +++ b/apps/auth/.gitignore @@ -0,0 +1,335 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# user-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Xcode template +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Gcc Patch +/*.gcno + +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +#out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### OpenSSL template +# OpenSSL-related files best not committed + +## Certificate Authority +*.ca + +## Certificate +*.crt + +## Certificate Sign Request +*.csr + +## Certificate +*.der + +## Key database file +*.kdb + +## OSCP request data +*.org + +## PKCS #12 +*.p12 + +## PEM-encoded certificate data +*.pem + +## Random number seed +*.rnd + +## SSLeay data +*.ssleay + +## S/MIME message +*.smime + + + + +*.env +*.env.backup + +*.save + +# compiled output +/dist +/node_modules + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +tokens diff --git a/apps/auth/Dockerfile b/apps/auth/Dockerfile new file mode 100644 index 0000000..f0b43cf --- /dev/null +++ b/apps/auth/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 +FROM node:18 + +WORKDIR /app + +COPY --chown=node:node . . + +RUN rm -f *.env *.env.* + +RUN apt-get update -y && apt-get install -y dumb-init + +RUN npm install + +ENV NODE_ENV=production + +RUN npm run prefullbuild && npm run prebuild && npm run build + +RUN mkdir temp temp/.prisma temp/@prisma temp/prisma && cp -r ./node_modules/.prisma/* ./temp/.prisma/ && cp -r ./node_modules/@prisma/* ./temp/@prisma/ && cp -r ./node_modules/prisma/* ./temp/prisma/ + +RUN rm -rdf node_modules + +RUN npm install --production + +RUN cp -r ./temp/* ./node_modules/ && rm -rdf temp + +RUN ls | grep -v node_modules | grep -v dist | xargs rm -rfv + +RUN cp -r ./dist/* ./ && rm -rdf dist + +USER node + +CMD ["dumb-init", "node", "./main.js"] diff --git a/apps/auth/LICENSE b/apps/auth/LICENSE new file mode 100644 index 0000000..d62cc9d --- /dev/null +++ b/apps/auth/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 SC (DJEEFT) Β© + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS β€œAS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/apps/auth/README.md b/apps/auth/README.md new file mode 100644 index 0000000..68f67e3 --- /dev/null +++ b/apps/auth/README.md @@ -0,0 +1,32 @@ +
+ +

IT Lab Platform - Auth Service

+
+ +
+ ✨ + + Π­Ρ‚Π° дирСктория содСрТит исходный ΠΊΠΎΠ΄ систСмы Π°ΡƒΠ½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ прилоТСния "IT Lab Application". + +
+ +## Ports +- `8082` - pg Admin +- `3005` - app + +## API +- /user + - /devices + - POST + - parameters: + - uuid: string(UUID) + - select?: + - name?: boolean + - fingerprint?: boolean + - isBlocked?: boolean + - return: + - Array: + - name?: string + - fingerprint?: string + - isBlocked?: boolean + - The select statement needs at least one truthy value. diff --git a/apps/auth/prisma/.gitignore b/apps/auth/prisma/.gitignore new file mode 100644 index 0000000..24a8e4d --- /dev/null +++ b/apps/auth/prisma/.gitignore @@ -0,0 +1 @@ +migrations diff --git a/apps/auth/serving/.gitignore b/apps/auth/serving/.gitignore new file mode 100644 index 0000000..f78e42d --- /dev/null +++ b/apps/auth/serving/.gitignore @@ -0,0 +1,225 @@ +*.env +.idea + +### NotepadPP template +# Notepad++ backups # +*.bak + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# user-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Xcode template +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Gcc Patch +/*.gcno + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Kate template +# Swap Files # +.*.kate-swp +.swp.* + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### SublimeText template +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json +sftp-config-alt*.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings diff --git a/apps/auth/serving/database.env.example b/apps/auth/serving/database.env.example new file mode 100644 index 0000000..4cfdb1b --- /dev/null +++ b/apps/auth/serving/database.env.example @@ -0,0 +1,3 @@ +POSTGRES_DB=user-authentication +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres diff --git a/apps/auth/serving/docker-compose.yaml b/apps/auth/serving/docker-compose.yaml new file mode 100644 index 0000000..1c0a532 --- /dev/null +++ b/apps/auth/serving/docker-compose.yaml @@ -0,0 +1,36 @@ +version: "3.9" + +services: + user-authentication-database: + image: postgres:latest + container_name: user-authentication-database + restart: always + networks: + - user-authentication + volumes: + - user-authentication-database:/var/lib/postgresql + env_file: + - ./database.env + user-authentication-database-admin: + container_name: user-authentication-database-admin + image: bitnami/phppgadmin:latest + restart: always + networks: + - user-authentication + depends_on: + - user-authentication-database + environment: + - DATABASE_HOST=user-authentication-database + ports: + - "8083:8080" + +networks: + user-authentication: + name: user-authentication + driver: bridge + +volumes: + user-authentication-database: + driver: local + user-authentication-tokens: + driver: local diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index a124382..d267063 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -12,7 +12,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix); - const port = process.env.PORT || 3000; + const port = process.env.PORT || 3005; await app.listen(port); Logger.log( `πŸš€ Application is running on: http://localhost:${port}/${globalPrefix}` diff --git a/apps/auth/src/assets/.gitkeep b/apps/auth/tokens.example/jwt.key similarity index 100% rename from apps/auth/src/assets/.gitkeep rename to apps/auth/tokens.example/jwt.key diff --git a/apps/auth/tokens.example/jwt.key.pub b/apps/auth/tokens.example/jwt.key.pub new file mode 100644 index 0000000..e69de29 diff --git a/apps/user-data/prisma/.gitignore b/apps/user-data/prisma/.gitignore new file mode 100644 index 0000000..24a8e4d --- /dev/null +++ b/apps/user-data/prisma/.gitignore @@ -0,0 +1 @@ +migrations diff --git a/apps/user-data/serving/.gitignore b/apps/user-data/serving/.gitignore index 7278035..f78e42d 100644 --- a/apps/user-data/serving/.gitignore +++ b/apps/user-data/serving/.gitignore @@ -20,7 +20,7 @@ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# User-specific stuff +# user-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml From 02fe538af484b971376e43f13690e079269b0f43 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:09:05 +0300 Subject: [PATCH 08/17] feat: added DTO classes for the auth service --- .../device/dto/elements/user-select.dto.ts | 15 ++++++++++++++ apps/auth/src/device/dto/fingerprint.dto.ts | 11 ++++++++++ apps/auth/src/device/dto/register.dto.ts | 18 +++++++++++++++++ apps/auth/src/device/dto/user.dto.ts | 20 +++++++++++++++++++ .../src/token/dto/generate-refresh.dto.ts | 19 ++++++++++++++++++ apps/auth/src/token/dto/refresh-token.dto.ts | 10 ++++++++++ apps/auth/src/user/dto/devices.dto.ts | 17 ++++++++++++++++ .../user/dto/elements/device-select.dto.ts | 19 ++++++++++++++++++ apps/auth/src/user/dto/uuid.dto.ts | 10 ++++++++++ 9 files changed, 139 insertions(+) create mode 100644 apps/auth/src/device/dto/elements/user-select.dto.ts create mode 100644 apps/auth/src/device/dto/fingerprint.dto.ts create mode 100644 apps/auth/src/device/dto/register.dto.ts create mode 100644 apps/auth/src/device/dto/user.dto.ts create mode 100644 apps/auth/src/token/dto/generate-refresh.dto.ts create mode 100644 apps/auth/src/token/dto/refresh-token.dto.ts create mode 100644 apps/auth/src/user/dto/devices.dto.ts create mode 100644 apps/auth/src/user/dto/elements/device-select.dto.ts create mode 100644 apps/auth/src/user/dto/uuid.dto.ts diff --git a/apps/auth/src/device/dto/elements/user-select.dto.ts b/apps/auth/src/device/dto/elements/user-select.dto.ts new file mode 100644 index 0000000..47738ea --- /dev/null +++ b/apps/auth/src/device/dto/elements/user-select.dto.ts @@ -0,0 +1,15 @@ +import { IsBoolean, IsOptional } from "class-validator"; + +export class UserSelectDto { + @IsOptional() + @IsBoolean() + public uuid?: boolean | null; + @IsOptional() + @IsBoolean() + public isBlocked?: boolean | null; + + constructor(uuid: boolean, isBlocked: boolean) { + this.uuid = uuid; + this.isBlocked = isBlocked; + } +} \ No newline at end of file diff --git a/apps/auth/src/device/dto/fingerprint.dto.ts b/apps/auth/src/device/dto/fingerprint.dto.ts new file mode 100644 index 0000000..d8c7001 --- /dev/null +++ b/apps/auth/src/device/dto/fingerprint.dto.ts @@ -0,0 +1,11 @@ +import { IsHexadecimal, Length } from "class-validator"; + +export class FingerprintDto { + @IsHexadecimal() + @Length(64, 64) + public fingerprint: string; + + constructor(fingerprint: string) { + this.fingerprint = fingerprint; + } +} \ No newline at end of file diff --git a/apps/auth/src/device/dto/register.dto.ts b/apps/auth/src/device/dto/register.dto.ts new file mode 100644 index 0000000..c99e977 --- /dev/null +++ b/apps/auth/src/device/dto/register.dto.ts @@ -0,0 +1,18 @@ +import { IsHexadecimal, IsString, IsUUID, Length } from "class-validator"; + +export class RegisterDto { + @IsHexadecimal() + @Length(64, 64) + public fingerprint: string; + @IsString() + @Length(0, 100) + public name: string; + @IsUUID(4) + public userUuid: string; + + constructor(fingerprint: string, name: string, userUuid: string) { + this.fingerprint = fingerprint; + this.name = name; + this.userUuid = userUuid; + } +} \ No newline at end of file diff --git a/apps/auth/src/device/dto/user.dto.ts b/apps/auth/src/device/dto/user.dto.ts new file mode 100644 index 0000000..87e75d1 --- /dev/null +++ b/apps/auth/src/device/dto/user.dto.ts @@ -0,0 +1,20 @@ +import { IsHexadecimal, IsOptional, Length, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { UserSelectDto } from "./elements/user-select.dto"; + +export class UserDto { + @IsHexadecimal() + @Length(64, 64) + public fingerprint: string; + @IsOptional() + @ValidateNested({ each: false }) + @Type(() => UserSelectDto) + public select?: UserSelectDto | null; + + constructor(fingerprint: string, select?: UserSelectDto | null) { + this.fingerprint = fingerprint; + this.select = select; + } +} diff --git a/apps/auth/src/token/dto/generate-refresh.dto.ts b/apps/auth/src/token/dto/generate-refresh.dto.ts new file mode 100644 index 0000000..ce4525b --- /dev/null +++ b/apps/auth/src/token/dto/generate-refresh.dto.ts @@ -0,0 +1,19 @@ +import { IsHexadecimal, IsOptional, IsString, IsUUID, Length } from "class-validator"; + +export class GenerateRefreshDto { + @IsUUID(4) + public user_uuid: string; + @IsHexadecimal() + @Length(64, 64) + public device_fingerprint: string; + @IsOptional() + @IsString() + @Length(0, 100) + public device_name?: string | null; + + constructor(user_uuid: string, device_fingerprint: string, device_name?: string) { + this.user_uuid = user_uuid; + this.device_fingerprint = device_fingerprint; + this.device_name = device_name; + } +} \ No newline at end of file diff --git a/apps/auth/src/token/dto/refresh-token.dto.ts b/apps/auth/src/token/dto/refresh-token.dto.ts new file mode 100644 index 0000000..c4066df --- /dev/null +++ b/apps/auth/src/token/dto/refresh-token.dto.ts @@ -0,0 +1,10 @@ +import { IsString } from "class-validator"; + +export class RefreshTokenDto { + @IsString() + public refresh_token: string; + + constructor(refresh_token: string) { + this.refresh_token = refresh_token; + } +} \ No newline at end of file diff --git a/apps/auth/src/user/dto/devices.dto.ts b/apps/auth/src/user/dto/devices.dto.ts new file mode 100644 index 0000000..7da5eef --- /dev/null +++ b/apps/auth/src/user/dto/devices.dto.ts @@ -0,0 +1,17 @@ +import { IsOptional, IsUUID, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { DeviceSelectDto } from "./elements/device-select.dto"; + +export class DevicesDto { + @IsUUID(4) + public uuid: string; + @IsOptional() + @ValidateNested({ each: false }) + @Type(() => DeviceSelectDto) + public select?: DeviceSelectDto | null; + + constructor(uuid: string, select: DeviceSelectDto) { + this.uuid = uuid; + this.select = select; + } +} \ No newline at end of file diff --git a/apps/auth/src/user/dto/elements/device-select.dto.ts b/apps/auth/src/user/dto/elements/device-select.dto.ts new file mode 100644 index 0000000..a301c28 --- /dev/null +++ b/apps/auth/src/user/dto/elements/device-select.dto.ts @@ -0,0 +1,19 @@ +import { IsBoolean, IsOptional } from "class-validator"; + +export class DeviceSelectDto { + @IsOptional() + @IsBoolean() + public name?: boolean | null; + @IsOptional() + @IsBoolean() + public fingerprint?: boolean | null; + @IsOptional() + @IsBoolean() + public isBlocked?: boolean | null; + + constructor(name: boolean, fingerprint: boolean, isBlocked: boolean) { + this.name = name; + this.fingerprint = fingerprint; + this.isBlocked = isBlocked; + } +} \ No newline at end of file diff --git a/apps/auth/src/user/dto/uuid.dto.ts b/apps/auth/src/user/dto/uuid.dto.ts new file mode 100644 index 0000000..fa6de64 --- /dev/null +++ b/apps/auth/src/user/dto/uuid.dto.ts @@ -0,0 +1,10 @@ +import { IsUUID } from "class-validator"; + +export class UuidDto { + @IsUUID(4) + public uuid: string; + + constructor(uuid: string) { + this.uuid = uuid; + } +} \ No newline at end of file From 30830155982b544a12d22775a48484a529da7f95 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:09:23 +0300 Subject: [PATCH 09/17] docs: create docs root dir --- docs/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/.gitkeep diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 From 3d0d30a24f0f4b825b8efad585bee47e3de1eb9b Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:10:02 +0300 Subject: [PATCH 10/17] feat: added configs for the auth service --- apps/auth/src/config/keys.config.ts | 10 ++++++++++ apps/auth/src/config/port.config.ts | 5 +++++ apps/auth/src/const/regex.ts | 2 ++ 3 files changed, 17 insertions(+) create mode 100644 apps/auth/src/config/keys.config.ts create mode 100644 apps/auth/src/config/port.config.ts create mode 100644 apps/auth/src/const/regex.ts diff --git a/apps/auth/src/config/keys.config.ts b/apps/auth/src/config/keys.config.ts new file mode 100644 index 0000000..9ee659f --- /dev/null +++ b/apps/auth/src/config/keys.config.ts @@ -0,0 +1,10 @@ +import { readFileSync } from "fs"; +import { join } from "path"; + +export default () => { + if (process.env.PRIVATE_KEY_PATH === undefined) throw new Error("Incorrect PRIVATE_KEY_PATH format in configurations"); + if (process.env.PUBLIC_KEY_PATH === undefined) throw new Error("Incorrect PUBLIC_KEY_PATH format in configurations"); + const private_key = readFileSync(join("./", process.env.PRIVATE_KEY_PATH), "utf8"); + const public_key = readFileSync(join("./", process.env.PUBLIC_KEY_PATH), "utf8"); + return { private_key, public_key }; +}; \ No newline at end of file diff --git a/apps/auth/src/config/port.config.ts b/apps/auth/src/config/port.config.ts new file mode 100644 index 0000000..6471b02 --- /dev/null +++ b/apps/auth/src/config/port.config.ts @@ -0,0 +1,5 @@ +export default () => { + const port = process.env.PORT !== undefined ? parseInt(process.env.PORT, 10) : 80; + if (isNaN(port) || port < 0) throw new Error("Incorrect port format in configurations"); + return { port }; +}; \ No newline at end of file diff --git a/apps/auth/src/const/regex.ts b/apps/auth/src/const/regex.ts new file mode 100644 index 0000000..c059203 --- /dev/null +++ b/apps/auth/src/const/regex.ts @@ -0,0 +1,2 @@ +export const UUIDv4 = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; +export const SHA256 = /^[a-f0-9]{64}$/i; \ No newline at end of file From 24885340d9e10b1e7125e9f66d6d06e2cd124ef8 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:10:27 +0300 Subject: [PATCH 11/17] feat: added prisma for the auth service --- apps/auth/prisma/schema.prisma | 27 +++++++++++++++++++ .../prisma/error/record-not-found.error.ts | 1 + .../interceptor/prisma-error.interceptor.ts | 26 ++++++++++++++++++ apps/auth/src/prisma/prisma.module.ts | 10 +++++++ .../src/prisma/service/prisma.service.spec.ts | 18 +++++++++++++ .../auth/src/prisma/service/prisma.service.ts | 15 +++++++++++ 6 files changed, 97 insertions(+) create mode 100644 apps/auth/prisma/schema.prisma create mode 100644 apps/auth/src/prisma/error/record-not-found.error.ts create mode 100644 apps/auth/src/prisma/interceptor/prisma-error.interceptor.ts create mode 100644 apps/auth/src/prisma/prisma.module.ts create mode 100644 apps/auth/src/prisma/service/prisma.service.spec.ts create mode 100644 apps/auth/src/prisma/service/prisma.service.ts diff --git a/apps/auth/prisma/schema.prisma b/apps/auth/prisma/schema.prisma new file mode 100644 index 0000000..2baa5df --- /dev/null +++ b/apps/auth/prisma/schema.prisma @@ -0,0 +1,27 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + uuid String @id @default(uuid()) @db.Uuid + isBlocked Boolean @default(false) + + devices UserDevice[] @relation("userDevice") +} + +model UserDevice { + fingerprint Bytes @id @db.ByteA + name String @db.VarChar(100) + userUuid String @db.Uuid + isBlocked Boolean @default(false) + + user User @relation("userDevice", fields: [userUuid], references: [uuid]) +} diff --git a/apps/auth/src/prisma/error/record-not-found.error.ts b/apps/auth/src/prisma/error/record-not-found.error.ts new file mode 100644 index 0000000..e1c9cb1 --- /dev/null +++ b/apps/auth/src/prisma/error/record-not-found.error.ts @@ -0,0 +1 @@ +export class RecordNotFoundError extends Error {} diff --git a/apps/auth/src/prisma/interceptor/prisma-error.interceptor.ts b/apps/auth/src/prisma/interceptor/prisma-error.interceptor.ts new file mode 100644 index 0000000..7ad4b9a --- /dev/null +++ b/apps/auth/src/prisma/interceptor/prisma-error.interceptor.ts @@ -0,0 +1,26 @@ +import { + CallHandler, + ConflictException, + ExecutionContext, + Injectable, + NestInterceptor, + NotFoundException +} from "@nestjs/common"; +import { catchError, Observable } from "rxjs"; +import { RecordNotFoundError } from "../error/record-not-found.error"; +import { UniqueConstraintFailedError } from "../error/unique-constraint-failed.error"; + +@Injectable() +export class PrismaErrorInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next + .handle() + .pipe( + catchError(err => { + if (err instanceof RecordNotFoundError) throw new NotFoundException(err.message); + if (err instanceof UniqueConstraintFailedError) throw new ConflictException(err.message); + throw err; + }) + ); + } +} diff --git a/apps/auth/src/prisma/prisma.module.ts b/apps/auth/src/prisma/prisma.module.ts new file mode 100644 index 0000000..44400fd --- /dev/null +++ b/apps/auth/src/prisma/prisma.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from "@nestjs/common"; +import { PrismaService } from "./service/prisma.service"; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService] +}) +export class PrismaModule { +} diff --git a/apps/auth/src/prisma/service/prisma.service.spec.ts b/apps/auth/src/prisma/service/prisma.service.spec.ts new file mode 100644 index 0000000..a68cb9e --- /dev/null +++ b/apps/auth/src/prisma/service/prisma.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from './prisma.service'; + +describe('PrismaService', () => { + let service: PrismaService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PrismaService], + }).compile(); + + service = module.get(PrismaService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/auth/src/prisma/service/prisma.service.ts b/apps/auth/src/prisma/service/prisma.service.ts new file mode 100644 index 0000000..aaa764a --- /dev/null +++ b/apps/auth/src/prisma/service/prisma.service.ts @@ -0,0 +1,15 @@ +import { INestApplication, Injectable, OnModuleInit } from "@nestjs/common"; +import { PrismaClient } from "@prisma/client"; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on("beforeExit", async () => { + await app.close(); + }); + } +} From dab8b2e84bf66ea74f313112cc626a94e10b6aa6 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:10:59 +0300 Subject: [PATCH 12/17] feat: added tocken module for the auth service --- apps/auth/src/token/service/token.service.ts | 122 +++++++++++++++++++ apps/auth/src/token/token.controller.ts | 36 ++++++ apps/auth/src/token/token.module.ts | 26 ++++ 3 files changed, 184 insertions(+) create mode 100644 apps/auth/src/token/service/token.service.ts create mode 100644 apps/auth/src/token/token.controller.ts create mode 100644 apps/auth/src/token/token.module.ts diff --git a/apps/auth/src/token/service/token.service.ts b/apps/auth/src/token/service/token.service.ts new file mode 100644 index 0000000..9bef4c8 --- /dev/null +++ b/apps/auth/src/token/service/token.service.ts @@ -0,0 +1,122 @@ +import { BadRequestException, ConflictException, Injectable, UnauthorizedException } from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; +import { UserService } from "../../user/service/user.service"; +import { DeviceService } from "../../device/service/device.service"; +import { PrismaService } from "../../Prisma/service/prisma.service"; +import { PrismaPromise } from "@prisma/client"; +import { SHA256, UUIDv4 } from "../../const/regex"; + +@Injectable() +export class TokenService { + constructor(private readonly jwt: JwtService, + private readonly user: UserService, + private readonly device: DeviceService, + private readonly prisma: PrismaService) { + } + + public async CheckRefresh(params: { refresh_token: string }) { + try { + await this.CheckRefreshTokenAssert(params); + return true; + } catch (e) { + return false; + } + } + + public async GenerateAccess(params: { refresh_token: string }) { + const data = await this.CheckRefreshTokenAssert(params); + return await this.jwt.signAsync({ + type: "access", + user_uuid: data.user_uuid, + device_fingerprint: data.device_fingerprint + }, { expiresIn: 600 }); + } + + public async GenerateRefresh(params: { user_uuid: string, device_fingerprint: string, device_name?: string | null }) { + const { is_user_blocked, is_device_blocked } = await this.GenerateRefreshBlocksChecker(params); + if (is_user_blocked) throw new ConflictException("The user is blocked"); + if (is_device_blocked) throw new ConflictException("The device is blocked"); + return await this.jwt.signAsync({ + type: "refresh", + user_uuid: params.user_uuid, + device_fingerprint: params.device_fingerprint + }, { + expiresIn: "7d" + }); + } + + private async JwtVerifyGuard(params: { jwt_token: string }) { + try { + return await this.jwt.verifyAsync(params.jwt_token); + } catch (e) { + throw new UnauthorizedException("Invalid token"); + } + } + + private async IsUserAndDeviceBlockedAssert(params: { user_uuid: string, device_fingerprint: string }) { + const res = await this.prisma.$transaction([ + this.user.isBlocked({ uuid: params.user_uuid }), + this.device.isBlocked({ fingerprint: params.device_fingerprint }), + this.device.getUserUuidAsPrismaPromise({ fingerprint: params.device_fingerprint }) + ]); + if (res[0] === null) throw new ConflictException("user with this uuid does not exist"); + if (res[1] === null || res[2] === null) throw new ConflictException("device with this fingerprint does not exist"); + if (res[0].isBlocked) throw new ConflictException("The user is blocked"); + if (res[1].isBlocked) throw new ConflictException("The device is blocked"); + if (res[2].userUuid !== params.user_uuid) throw new ConflictException("The device is connected to another user"); + } + + private async CheckRefreshTokenAssert(params: { refresh_token: string }) { + const data = await this.JwtVerifyGuard<{ user_uuid: string, device_fingerprint: string, type: string }>({ jwt_token: params.refresh_token }); + if (typeof data !== "object") throw new BadRequestException("JWT payload does not object"); + if (typeof data.user_uuid !== "string") throw new BadRequestException("user_uuid field does not string"); + if (!UUIDv4.test(data.user_uuid)) throw new BadRequestException("user_uuid field does not uuid"); + if (typeof data.device_fingerprint !== "string") throw new BadRequestException("device_fingerprint field does not string"); + if (!SHA256.test(data.device_fingerprint)) throw new BadRequestException("device_fingerprint field does not sha256"); + if (typeof data.type !== "string") throw new BadRequestException("type field does not string"); + if (data.type !== "refresh") throw new BadRequestException("type field does not equal \"refresh\" value"); + await this.IsUserAndDeviceBlockedAssert(data); + return data; + } + + private async GenerateRefreshBlocksChecker(params: { user_uuid: string, device_fingerprint: string, device_name?: string | null }) { + const transaction: PrismaPromise[] = []; + const user_exists = await this.user.isExists({ uuid: params.user_uuid }); + const device_exists = await this.device.isExists({ fingerprint: params.device_fingerprint }); + if (user_exists) { + if (device_exists) { + if ((await this.device.getUserUuidAsPrismaPromise({ fingerprint: params.device_fingerprint }))?.userUuid !== params.user_uuid) + throw new ConflictException("The device is connected to another user"); + } else { + if (params.device_name === null || params.device_name === undefined) + throw new ConflictException("The device does not exist, but the new device name is not specified"); + transaction.push( + this.device.register({ + userUuid: params.user_uuid, + fingerprint: params.device_fingerprint, + name: params.device_name + })); + } + } else { + if (device_exists) + throw new ConflictException("The device is connected to another user"); + if (params.device_name === null || params.device_name === undefined) + throw new ConflictException("The device does not exist, but the new device name is not specified"); + transaction.push( + this.user.register({ uuid: params.user_uuid }), + this.device.register({ + userUuid: params.user_uuid, + fingerprint: params.device_fingerprint, + name: params.device_name + })); + } + transaction.push( + this.user.isBlocked({ uuid: params.user_uuid }), + this.device.isBlocked({ fingerprint: params.device_fingerprint }) + ); + const transaction_res = await this.prisma.$transaction(transaction); + const is_user_blocked = transaction_res[transaction_res.length - 2]?.isBlocked as boolean | undefined; + const is_device_blocked = transaction_res[transaction_res.length - 1]?.isBlocked as boolean | undefined; + return { is_user_blocked, is_device_blocked }; + } +} diff --git a/apps/auth/src/token/token.controller.ts b/apps/auth/src/token/token.controller.ts new file mode 100644 index 0000000..4859075 --- /dev/null +++ b/apps/auth/src/token/token.controller.ts @@ -0,0 +1,36 @@ +import { Body, Controller, Get, HttpCode, Post, ValidationPipe } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { TokenService } from "./service/token.service"; +import { GenerateRefreshDto } from "./dto/generate-refresh.dto"; +import { RefreshTokenDto } from "./dto/refresh-token.dto"; + +@Controller("token") +export class TokenController { + constructor(private config: ConfigService<{ port: number, private_key: string, public_key: string }>, + private token: TokenService) { + } + + @Post("generate/refresh") + @HttpCode(200) + GenerateRefresh(@Body(new ValidationPipe()) params: GenerateRefreshDto) { + return this.token.GenerateRefresh(params); + } + + @Post("generate/access") + @HttpCode(200) + GenerateAccess(@Body(new ValidationPipe()) params: RefreshTokenDto) { + return this.token.GenerateAccess(params); + } + + @Post("is-valid/refresh") + @HttpCode(200) + CheckRefresh(@Body(new ValidationPipe()) params: RefreshTokenDto) { + return this.token.CheckRefresh(params); + } + + @Get("public-key") + @HttpCode(200) + async GetAccessPublicKey() { + return this.config.get("public_key"); + } +} \ No newline at end of file diff --git a/apps/auth/src/token/token.module.ts b/apps/auth/src/token/token.module.ts new file mode 100644 index 0000000..8a4cc8a --- /dev/null +++ b/apps/auth/src/token/token.module.ts @@ -0,0 +1,26 @@ +import { Module } from "@nestjs/common"; +import { TokenController } from "./token.controller"; +import { JwtModule } from "@nestjs/jwt"; +import { ConfigModule, ConfigService } from "@nestjs/config"; +import { TokenService } from "./service/token.service"; +import { UserModule } from "../user/user.module"; +import { DeviceModule } from "../device/device.module"; + +@Module({ + imports: [ + UserModule, + DeviceModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService<{ port: number, private_key: string, public_key: string }>) => ({ + publicKey: configService.get("public_key"), + privateKey: configService.get("private_key"), + signOptions: { algorithm: "RS512", issuer: "sc-user-authentication" } + }) + })], + controllers: [TokenController], + providers: [TokenService] +}) +export class TokenModule { +} From b68236052dbebe0d203ac520483d091789c847c0 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:11:09 +0300 Subject: [PATCH 13/17] feat: added ucf prisma for the auth service --- apps/auth/src/prisma/error/unique-constraint-failed.error.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/auth/src/prisma/error/unique-constraint-failed.error.ts diff --git a/apps/auth/src/prisma/error/unique-constraint-failed.error.ts b/apps/auth/src/prisma/error/unique-constraint-failed.error.ts new file mode 100644 index 0000000..647b582 --- /dev/null +++ b/apps/auth/src/prisma/error/unique-constraint-failed.error.ts @@ -0,0 +1 @@ +export class UniqueConstraintFailedError extends Error {} From c95e55f625e09adb9755b414e1861bdfc8809092 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:11:23 +0300 Subject: [PATCH 14/17] feat: added device module for the auth service --- apps/auth/src/device/device.controller.ts | 84 +++++++++++++++++++ apps/auth/src/device/device.module.ts | 11 +++ .../auth/src/device/service/device.service.ts | 64 ++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 apps/auth/src/device/device.controller.ts create mode 100644 apps/auth/src/device/device.module.ts create mode 100644 apps/auth/src/device/service/device.service.ts diff --git a/apps/auth/src/device/device.controller.ts b/apps/auth/src/device/device.controller.ts new file mode 100644 index 0000000..dd29534 --- /dev/null +++ b/apps/auth/src/device/device.controller.ts @@ -0,0 +1,84 @@ +import { Body, ConflictException, Controller, HttpCode, Post, ValidationPipe } from "@nestjs/common"; +import { DeviceService } from "./service/device.service"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import { RegisterDto } from "./dto/register.dto"; +import { FingerprintDto } from "./dto/fingerprint.dto"; +import { UserDto } from "./dto/user.dto"; + +@Controller("device") +export class DeviceController { + constructor(private readonly device: DeviceService) { + } + + @Post("user") + @HttpCode(200) + async User(@Body(new ValidationPipe()) params: UserDto) { + return this.device.User(params); + } + + @Post("is-exists") + @HttpCode(200) + async IsExists(@Body(new ValidationPipe()) params: FingerprintDto) { + return await this.device.isExists(params); + } + + @Post("register") + @HttpCode(200) + async Register(@Body(new ValidationPipe()) params: RegisterDto) { + try { + await this.device.register(params); + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) throw e; + if (e.code === "P2002") throw new ConflictException("device with the same fingerprint already exists"); + if (e.code === "P2003") throw new ConflictException("user with this uuid does not exist"); + throw e; + } + } + + + @Post("delete") + @HttpCode(200) + async Delete(@Body(new ValidationPipe()) params: FingerprintDto) { + try { + await this.device.delete(params); + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) throw e; + if (e.code !== "P2025") throw e; + throw new ConflictException("device with this fingerprint does not exist"); + } + } + + + @Post("block") + @HttpCode(200) + async Block(@Body(new ValidationPipe()) params: FingerprintDto) { + try { + await this.device.block(params); + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) throw e; + if (e.code !== "P2025") throw e; + throw new ConflictException("device with this fingerprint does not exist"); + } + } + + @Post("unblock") + @HttpCode(200) + async Unblock(@Body(new ValidationPipe()) params: FingerprintDto) { + try { + await this.device.unblock(params); + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) throw e; + if (e.code !== "P2025") throw e; + throw new ConflictException("device with this fingerprint does not exist"); + } + } + + + @Post("is-blocked") + @HttpCode(200) + async IsBlocked(@Body(new ValidationPipe()) params: FingerprintDto) { + const isBlocked = await this.device.isBlocked(params); + if (isBlocked !== null) return isBlocked.isBlocked; + throw new ConflictException("device with this fingerprint does not exist"); + } +} diff --git a/apps/auth/src/device/device.module.ts b/apps/auth/src/device/device.module.ts new file mode 100644 index 0000000..706f553 --- /dev/null +++ b/apps/auth/src/device/device.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { DeviceController } from "./device.controller"; +import { DeviceService } from "./service/device.service"; + +@Module({ + providers: [DeviceService], + controllers: [DeviceController], + exports: [DeviceService] +}) +export class DeviceModule { +} \ No newline at end of file diff --git a/apps/auth/src/device/service/device.service.ts b/apps/auth/src/device/service/device.service.ts new file mode 100644 index 0000000..5e0e462 --- /dev/null +++ b/apps/auth/src/device/service/device.service.ts @@ -0,0 +1,64 @@ +import { BadRequestException, ConflictException, Injectable } from "@nestjs/common"; +import { PrismaService } from "../../Prisma/service/prisma.service"; +import { UserDto } from "../dto/user.dto"; +import { PrismaClientValidationError } from "@prisma/client/runtime"; + +@Injectable() +export class DeviceService { + constructor(private readonly prisma: PrismaService) { + } + + public getUserUuidAsPrismaPromise(params: { fingerprint: string }) { + const fingerprint = Buffer.from(params.fingerprint, "hex"); + return this.prisma.userDevice.findUnique({ where: { fingerprint }, select: { userUuid: true } }); + } + + public async User(params: UserDto) { + const fingerprint = Buffer.from(params.fingerprint, "hex"); + let select: { uuid?: boolean, isBlocked?: boolean } | undefined = { uuid: true, isBlocked: true }; + if (params.select !== undefined && params.select !== null) { + select = {}; + if (params.select.uuid !== undefined && params.select.uuid !== null) select.uuid = params.select.uuid; + if (params.select.isBlocked !== undefined && params.select.isBlocked !== null) select.isBlocked = params.select.isBlocked; + } + try { + const res: { user: { uuid?: string, isBlocked?: boolean } } | null = + await this.prisma.userDevice.findUnique({ where: { fingerprint }, select: { user: { select } } }); + if (res === null) throw new ConflictException("device with this fingerprint does not exist"); + return res.user; + } catch (e) { + if (!(e instanceof PrismaClientValidationError)) throw e; + throw new BadRequestException("The select statement needs at least one truthy value."); + } + } + + public async isExists(params: { fingerprint: string }) { + const fingerprint = Buffer.from(params.fingerprint, "hex"); + return (await this.prisma.userDevice.findUnique({ where: { fingerprint }, select: null }) !== null); + } + + public register(params: { userUuid: string, name: string, fingerprint: string }) { + const fingerprint = Buffer.from(params.fingerprint, "hex"); + return this.prisma.userDevice.create({ data: { ...params, fingerprint }, select: null }); + } + + public async delete(params: { fingerprint: string }) { + const fingerprint = Buffer.from(params.fingerprint, "hex"); + await this.prisma.userDevice.delete({ where: { fingerprint }, select: null }); + } + + public async block(params: { fingerprint: string }) { + const fingerprint = Buffer.from(params.fingerprint, "hex"); + await this.prisma.userDevice.update({ where: { fingerprint }, data: { isBlocked: true }, select: null }); + } + + public async unblock(params: { fingerprint: string }) { + const fingerprint = Buffer.from(params.fingerprint, "hex"); + await this.prisma.userDevice.update({ where: { fingerprint }, data: { isBlocked: false }, select: null }); + } + + public isBlocked(params: { fingerprint: string }) { + const fingerprint = Buffer.from(params.fingerprint, "hex"); + return this.prisma.userDevice.findUnique({ where: { fingerprint }, select: { isBlocked: true } }); + } +} From 1ab078fc54ad8121eca313c0c44625e557d9ad36 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:11:33 +0300 Subject: [PATCH 15/17] feat: added user module for the auth service --- apps/auth/src/user/service/user.service.ts | 54 +++++++++++++++ apps/auth/src/user/user.controller.ts | 80 ++++++++++++++++++++++ apps/auth/src/user/user.module.ts | 11 +++ 3 files changed, 145 insertions(+) create mode 100644 apps/auth/src/user/service/user.service.ts create mode 100644 apps/auth/src/user/user.controller.ts create mode 100644 apps/auth/src/user/user.module.ts diff --git a/apps/auth/src/user/service/user.service.ts b/apps/auth/src/user/service/user.service.ts new file mode 100644 index 0000000..0c9008d --- /dev/null +++ b/apps/auth/src/user/service/user.service.ts @@ -0,0 +1,54 @@ +import { BadRequestException, Injectable } from "@nestjs/common"; +import { PrismaService } from "../../Prisma/service/prisma.service"; +import { DevicesDto } from "../dto/devices.dto"; +import { PrismaClientValidationError } from "@prisma/client/runtime"; + +@Injectable() +export class UserService { + constructor(private readonly prisma: PrismaService) { + } + + public async devices(params: DevicesDto): Promise<{ name?: string, fingerprint?: string, isBlocked?: boolean }[]> { + let select: { name?: boolean, fingerprint?: boolean, isBlocked?: boolean } | undefined = { + name: true, fingerprint: true, isBlocked: true + }; + if (params.select !== undefined && params.select !== null) { + select = {}; + if (params.select.name !== undefined && params.select.name !== null) select.name = params.select.name; + if (params.select.fingerprint !== undefined && params.select.fingerprint !== null) select.fingerprint = params.select.fingerprint; + if (params.select.isBlocked !== undefined && params.select.isBlocked !== null) select.isBlocked = params.select.isBlocked; + } + try { + const res: { name?: string, isBlocked?: boolean, fingerprint?: Buffer }[] = + await this.prisma.userDevice.findMany({ where: { userUuid: params.uuid }, select }); + return res.map(value => ({ ...value, fingerprint: value.fingerprint?.toString("hex") })); + } catch (e) { + if (!(e instanceof PrismaClientValidationError)) throw e; + throw new BadRequestException("The select statement needs at least one truthy value."); + } + } + + public async isExists(params: { uuid: string }) { + return (await this.prisma.user.findUnique({ where: params, select: null }) !== null); + } + + public register(params: { uuid: string }) { + return this.prisma.user.create({ data: params, select: null }); + } + + public async delete(params: { uuid: string }) { + await this.prisma.user.delete({ where: params, select: null }); + } + + public async block(params: { uuid: string }) { + await this.prisma.user.update({ where: params, data: { isBlocked: true }, select: null }); + } + + public async unblock(params: { uuid: string }) { + await this.prisma.user.update({ where: params, data: { isBlocked: false }, select: null }); + } + + public isBlocked(params: { uuid: string }) { + return this.prisma.user.findUnique({ where: params, select: { isBlocked: true } }); + } +} \ No newline at end of file diff --git a/apps/auth/src/user/user.controller.ts b/apps/auth/src/user/user.controller.ts new file mode 100644 index 0000000..da3ad07 --- /dev/null +++ b/apps/auth/src/user/user.controller.ts @@ -0,0 +1,80 @@ +import { Body, ConflictException, Controller, HttpCode, Post, ValidationPipe } from "@nestjs/common"; +import { UserService } from "./service/user.service"; +import { UuidDto } from "./dto/uuid.dto"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import { DevicesDto } from "./dto/devices.dto"; + +@Controller("user") +export class UserController { + constructor(private readonly user: UserService) { + } + + @Post("devices") + @HttpCode(200) + async Devices(@Body(new ValidationPipe()) params: DevicesDto) { + if (await this.user.isExists({ uuid: params.uuid })) return await this.user.devices(params); + else throw new ConflictException("user with this uuid does not exist"); + } + + @Post("is-exists") + @HttpCode(200) + async IsExists(@Body(new ValidationPipe()) params: UuidDto) { + return await this.user.isExists(params); + } + + @Post("register") + @HttpCode(200) + async Register(@Body(new ValidationPipe()) params: UuidDto) { + try { + await this.user.register(params); + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) throw e; + if (e.code !== "P2002") throw e; + throw new ConflictException("user with the same uuid already exists"); + } + } + + @Post("delete") + @HttpCode(200) + async Delete(@Body(new ValidationPipe()) params: UuidDto) { + try { + await this.user.delete(params); + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) throw e; + if (e.code !== "P2025") throw e; + throw new ConflictException("user with this uuid does not exist"); + } + } + + @Post("block") + @HttpCode(200) + async Block(@Body(new ValidationPipe()) params: UuidDto) { + try { + await this.user.block(params); + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) throw e; + if (e.code !== "P2025") throw e; + throw new ConflictException("user with this uuid does not exist"); + } + } + + @Post("unblock") + @HttpCode(200) + async Unblock(@Body(new ValidationPipe()) params: UuidDto) { + try { + await this.user.unblock(params); + } catch (e) { + if (!(e instanceof PrismaClientKnownRequestError)) throw e; + if (e.code !== "P2025") throw e; + throw new ConflictException("user with this uuid does not exist"); + } + } + + @Post("is-blocked") + @HttpCode(200) + async IsBlocked(@Body(new ValidationPipe()) params: UuidDto) { + const isBlocked = await this.user.isBlocked(params); + if (isBlocked !== null) return isBlocked.isBlocked; + throw new ConflictException("user with this uuid does not exist"); + } +} diff --git a/apps/auth/src/user/user.module.ts b/apps/auth/src/user/user.module.ts new file mode 100644 index 0000000..f246f20 --- /dev/null +++ b/apps/auth/src/user/user.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { UserController } from "./user.controller"; +import { UserService } from "./service/user.service"; + +@Module({ + providers: [UserService], + controllers: [UserController], + exports: [UserService] +}) +export class UserModule { +} \ No newline at end of file From f5d32f6789e18c659c28d7addf1d452fb78713a0 Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:11:55 +0300 Subject: [PATCH 16/17] feat: added submodules in the app.module.ts for the auth service --- apps/auth/src/app/app.module.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/auth/src/app/app.module.ts b/apps/auth/src/app/app.module.ts index 6a9bc16..d9898fc 100644 --- a/apps/auth/src/app/app.module.ts +++ b/apps/auth/src/app/app.module.ts @@ -1,10 +1,23 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from "@nestjs/config"; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import PortConfig from "../config/port.config"; +import KeysConfig from "../config/keys.config"; +import { UserModule } from "../user/user.module"; +import { DeviceModule } from "../device/device.module"; +import { TokenModule } from "../token/token.module"; +import { PrismaModule } from "../prisma/prisma.module"; @Module({ - imports: [], + imports: [ + ConfigModule.forRoot({ isGlobal: true, load: [PortConfig, KeysConfig] }), + UserModule, + DeviceModule, + TokenModule, + PrismaModule + ], controllers: [AppController], providers: [AppService], }) From 853523cc2d0929b9100501a4a870c9131338a94e Mon Sep 17 00:00:00 2001 From: Artem-Darius Atlas Date: Thu, 29 Jun 2023 16:14:31 +0300 Subject: [PATCH 17/17] docs: updated graphs --- README.md | 5 ++++- apps/auth/README.md | 8 ++++++++ apps/user-data/README.md | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1233672..b94562f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,10 @@ graph TD; agw-->user-data; user-data-->user-data-db; user-data-db-->user-data-pg-admin; - agw-->auth; + agw-->auth-client; + auth-client-->auth; + auth-->auth-db; + auth-db-->auth-db-admin; ``` ## Generate code diff --git a/apps/auth/README.md b/apps/auth/README.md index 68f67e3..3e34631 100644 --- a/apps/auth/README.md +++ b/apps/auth/README.md @@ -10,6 +10,14 @@ +## Structure +```mermaid +graph TD; + agw-->auth; + auth-->auth-db; + auth-db-->auth-db-admin; +``` + ## Ports - `8082` - pg Admin - `3005` - app diff --git a/apps/user-data/README.md b/apps/user-data/README.md index 7b3eee9..aca5a0d 100644 --- a/apps/user-data/README.md +++ b/apps/user-data/README.md @@ -13,3 +13,11 @@ ## Ports - `8083` - pg Admin - `3002` - app + +## Structure +```mermaid +graph TD; + agw-->user-data; + user-data-->user-data-db; + user-data-db-->user-data-pg-admin; +```