From dda39facecf924ead7af61035cfa4a3630b7ec2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 2 Dec 2025 10:03:03 +0100 Subject: [PATCH 01/33] chore: initialize nestjs dedicated app --- apps/server-nestjs/.gitignore | 56 + apps/server-nestjs/.prettierrc | 4 + apps/server-nestjs/README.md | 98 + apps/server-nestjs/eslint.config.mjs | 35 + apps/server-nestjs/nest-cli.json | 8 + apps/server-nestjs/package.json | 71 + apps/server-nestjs/src/app.controller.spec.ts | 22 + apps/server-nestjs/src/app.controller.ts | 12 + apps/server-nestjs/src/app.module.ts | 10 + apps/server-nestjs/src/app.service.ts | 8 + apps/server-nestjs/src/main.ts | 8 + apps/server-nestjs/test/app.e2e-spec.ts | 25 + apps/server-nestjs/test/jest-e2e.json | 9 + apps/server-nestjs/tsconfig.build.json | 4 + apps/server-nestjs/tsconfig.json | 25 + pnpm-lock.yaml | 3766 ++++++++++++++++- 16 files changed, 4032 insertions(+), 129 deletions(-) create mode 100644 apps/server-nestjs/.gitignore create mode 100644 apps/server-nestjs/.prettierrc create mode 100644 apps/server-nestjs/README.md create mode 100644 apps/server-nestjs/eslint.config.mjs create mode 100644 apps/server-nestjs/nest-cli.json create mode 100644 apps/server-nestjs/package.json create mode 100644 apps/server-nestjs/src/app.controller.spec.ts create mode 100644 apps/server-nestjs/src/app.controller.ts create mode 100644 apps/server-nestjs/src/app.module.ts create mode 100644 apps/server-nestjs/src/app.service.ts create mode 100644 apps/server-nestjs/src/main.ts create mode 100644 apps/server-nestjs/test/app.e2e-spec.ts create mode 100644 apps/server-nestjs/test/jest-e2e.json create mode 100644 apps/server-nestjs/tsconfig.build.json create mode 100644 apps/server-nestjs/tsconfig.json diff --git a/apps/server-nestjs/.gitignore b/apps/server-nestjs/.gitignore new file mode 100644 index 000000000..4b56acfbe --- /dev/null +++ b/apps/server-nestjs/.gitignore @@ -0,0 +1,56 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# 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 + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/apps/server-nestjs/.prettierrc b/apps/server-nestjs/.prettierrc new file mode 100644 index 000000000..a20502b7f --- /dev/null +++ b/apps/server-nestjs/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md new file mode 100644 index 000000000..d30c94649 --- /dev/null +++ b/apps/server-nestjs/README.md @@ -0,0 +1,98 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Project setup + +```bash +$ pnpm install +``` + +## Compile and run the project + +```bash +# development +$ pnpm run start + +# watch mode +$ pnpm run start:dev + +# production mode +$ pnpm run start:prod +``` + +## Run tests + +```bash +# unit tests +$ pnpm run test + +# e2e tests +$ pnpm run test:e2e + +# test coverage +$ pnpm run test:cov +``` + +## Deployment + +When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. + +If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: + +```bash +$ pnpm install -g @nestjs/mau +$ mau deploy +``` + +With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/apps/server-nestjs/eslint.config.mjs b/apps/server-nestjs/eslint.config.mjs new file mode 100644 index 000000000..4e9f8271c --- /dev/null +++ b/apps/server-nestjs/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + "prettier/prettier": ["error", { endOfLine: "auto" }], + }, + }, +); diff --git a/apps/server-nestjs/nest-cli.json b/apps/server-nestjs/nest-cli.json new file mode 100644 index 000000000..f9aa683b1 --- /dev/null +++ b/apps/server-nestjs/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/server-nestjs/package.json b/apps/server-nestjs/package.json new file mode 100644 index 000000000..49db86c0f --- /dev/null +++ b/apps/server-nestjs/package.json @@ -0,0 +1,71 @@ +{ + "name": "server-nestjs", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/node": "^22.10.7", + "@types/supertest": "^6.0.2", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^16.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/apps/server-nestjs/src/app.controller.spec.ts b/apps/server-nestjs/src/app.controller.spec.ts new file mode 100644 index 000000000..d22f3890a --- /dev/null +++ b/apps/server-nestjs/src/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 appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService], + }).compile(); + + appController = app.get(AppController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/apps/server-nestjs/src/app.controller.ts b/apps/server-nestjs/src/app.controller.ts new file mode 100644 index 000000000..cce879ee6 --- /dev/null +++ b/apps/server-nestjs/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHello(): string { + return this.appService.getHello(); + } +} diff --git a/apps/server-nestjs/src/app.module.ts b/apps/server-nestjs/src/app.module.ts new file mode 100644 index 000000000..86628031c --- /dev/null +++ b/apps/server-nestjs/src/app.module.ts @@ -0,0 +1,10 @@ +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/server-nestjs/src/app.service.ts b/apps/server-nestjs/src/app.service.ts new file mode 100644 index 000000000..927d7cca0 --- /dev/null +++ b/apps/server-nestjs/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/apps/server-nestjs/src/main.ts b/apps/server-nestjs/src/main.ts new file mode 100644 index 000000000..f76bc8d97 --- /dev/null +++ b/apps/server-nestjs/src/main.ts @@ -0,0 +1,8 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(process.env.PORT ?? 3000); +} +bootstrap(); diff --git a/apps/server-nestjs/test/app.e2e-spec.ts b/apps/server-nestjs/test/app.e2e-spec.ts new file mode 100644 index 000000000..36852c54f --- /dev/null +++ b/apps/server-nestjs/test/app.e2e-spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/server-nestjs/test/jest-e2e.json b/apps/server-nestjs/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/server-nestjs/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/server-nestjs/tsconfig.build.json b/apps/server-nestjs/tsconfig.build.json new file mode 100644 index 000000000..64f86c6bd --- /dev/null +++ b/apps/server-nestjs/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/apps/server-nestjs/tsconfig.json b/apps/server-nestjs/tsconfig.json new file mode 100644 index 000000000..aba29b0e7 --- /dev/null +++ b/apps/server-nestjs/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af807b2a1..933c18333 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,7 +191,7 @@ importers: version: 2.1.9(@types/node@24.10.0)(terser@5.44.1) vite-plugin-pwa: specifier: ^1.1.0 - version: 1.1.0(vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1))(workbox-build@7.3.0)(workbox-window@7.3.0) + version: 1.1.0(vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vitest: specifier: ^2.1.8 version: 2.1.9(@types/node@24.10.0)(jsdom@25.0.1)(terser@5.44.1) @@ -366,6 +366,94 @@ importers: specifier: ^2.1.8 version: 2.1.9(@types/node@24.10.0)(jsdom@25.0.1)(terser@5.44.1) + apps/server-nestjs: + dependencies: + '@nestjs/common': + specifier: ^11.0.1 + version: 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.0.1 + version: 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.0.1 + version: 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + devDependencies: + '@eslint/eslintrc': + specifier: ^3.2.0 + version: 3.3.1 + '@eslint/js': + specifier: ^9.18.0 + version: 9.39.1 + '@nestjs/cli': + specifier: ^11.0.0 + version: 11.0.14(@types/node@22.19.3) + '@nestjs/schematics': + specifier: ^11.0.0 + version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + '@nestjs/testing': + specifier: ^11.0.1 + version: 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11) + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^22.10.7 + version: 22.19.3 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.3 + eslint: + specifier: ^9.18.0 + version: 9.39.1(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.2 + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4) + globals: + specifier: ^16.0.0 + version: 16.5.0 + jest: + specifier: ^30.0.0 + version: 30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + prettier: + specifier: ^3.4.2 + version: 3.7.4 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + supertest: + specifier: ^7.0.0 + version: 7.2.1 + ts-jest: + specifier: ^29.2.5 + version: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) + ts-loader: + specifier: ^9.5.2 + version: 9.5.4(typescript@5.9.3)(webpack@5.103.0) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.19.3)(typescript@5.9.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.20.0 + version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + packages/eslintconfig: devDependencies: '@antfu/eslint-config': @@ -964,6 +1052,37 @@ packages: openapi3-ts: ^2.0.0 || ^3.0.0 zod: ^3.20.0 + '@angular-devkit/core@19.2.17': + resolution: {integrity: sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/core@19.2.19': + resolution: {integrity: sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.19': + resolution: {integrity: sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@19.2.17': + resolution: {integrity: sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/schematics@19.2.19': + resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@antfu/eslint-config@3.16.0': resolution: {integrity: sha512-g6RAXUMeow9vexoOMYwCpByY2xSDpAD78q+rvQLvVpY6MFcxFD/zmdrZGYa/yt7LizK86m17kIYKOGLJ3L8P0w==} hasBin: true @@ -1177,6 +1296,27 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.27.1': resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} @@ -1189,6 +1329,70 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} @@ -1552,6 +1756,9 @@ packages: '@biomejs/wasm-nodejs@2.2.6': resolution: {integrity: sha512-lUEcvW+2eyMTgCofknBT04AvY7KkQSqKe3Nv40+ZxWVlStsPB0v2RWLu7xks69Yxcb3TfNGsfq21OWkdrmO2NQ==} + '@borewit/text-codec@0.2.1': + resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} + '@cacheable/memoize@2.0.3': resolution: {integrity: sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==} @@ -1567,6 +1774,10 @@ packages: '@clack/prompts@0.9.1': resolution: {integrity: sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==} + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@commitlint/cli@19.8.1': resolution: {integrity: sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==} engines: {node: '>=v18'} @@ -1636,6 +1847,10 @@ packages: resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} engines: {node: '>=v18'} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -2189,6 +2404,149 @@ packages: peerDependencies: vue: '>=3' + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -2205,10 +2563,96 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jest/console@30.2.0': + resolution: {integrity: sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.2.0': + resolution: {integrity: sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@30.2.0': + resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.2.0': + resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.2.0': + resolution: {integrity: sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.2.0': + resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.2.0': + resolution: {integrity: sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.2.0': + resolution: {integrity: sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.2.0': + resolution: {integrity: sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.2.0': + resolution: {integrity: sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.2.0': + resolution: {integrity: sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@30.2.0': + resolution: {integrity: sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.2.0': + resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2228,6 +2672,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jsep-plugin/assignment@1.3.0': resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} engines: {node: '>= 10.16.0'} @@ -2272,6 +2719,10 @@ packages: '@kubernetes/client-node@0.22.3': resolution: {integrity: sha512-dG8uah3+HDJLpJEESshLRZlAZ4PgDeV9mZXT0u1g7oy4KMRzdZ7n5g0JEIlL6QhK51/2ztcIqURAnjfjJt6Z+g==} + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + '@lukeed/ms@2.0.2': resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} @@ -2279,6 +2730,78 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@nestjs/cli@11.0.14': + resolution: {integrity: sha512-YwP03zb5VETTwelXU+AIzMVbEZKk/uxJL+z9pw0mdG9ogAtqZ6/mpmIM4nEq/NU8D0a7CBRLcMYUmWW/55pfqw==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + + '@nestjs/common@11.1.11': + resolution: {integrity: sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/core@11.1.11': + resolution: {integrity: sha512-H9i+zT3RvHi7tDc+lCmWHJ3ustXveABCr+Vcpl96dNOxgmrx4elQSTC4W93Mlav2opfLV+p0UTHY6L+bpUA4zA==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/platform-express@11.1.11': + resolution: {integrity: sha512-kyABSskdMRIAMWL0SlbwtDy4yn59RL4HDdwHDz/fxWuv7/53YP8Y2DtV3/sHqY5Er0msMVTZrM38MjqXhYL7gw==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + + '@nestjs/schematics@11.0.9': + resolution: {integrity: sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==} + peerDependencies: + typescript: '>=4.8.2' + + '@nestjs/testing@11.1.11': + resolution: {integrity: sha512-Po2aZKXlxuySDEh3Gi05LJ7/BtfTAPRZ3KPTrbpNrTmgGr3rFgEGYpQwN50wXYw0pywoICiFLZSZ/qXsplf6NA==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2291,6 +2814,14 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@nuxt/opencollective@0.4.1': + resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} + engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} + hasBin: true + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2505,6 +3036,15 @@ packages: cpu: [x64] os: [win32] + '@sinclair/typebox@0.34.46': + resolution: {integrity: sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@13.0.5': + resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -2520,6 +3060,13 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@ts-rest/core@3.52.1': resolution: {integrity: sha512-tAjz7Kxq/grJodcTA1Anop4AVRDlD40fkksEV5Mmal88VoZeRKAG8oMHsDwdwPZz+B/zgnz0q2sF+cm5M7Bc7g==} peerDependencies: @@ -2547,28 +3094,88 @@ packages: '@ts-rest/core': ~3.52.0 zod: ^3.22.3 + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/conventional-commits-parser@5.0.2': resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/estree@0.0.39': resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/js-yaml@4.0.9': - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/express-serve-static-core@5.1.0': + resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} - '@types/jsdom@21.1.7': - resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - '@types/json-schema@7.0.15': + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/lodash@4.17.20': @@ -2577,24 +3184,51 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.19.3': + resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} + '@types/node@24.10.0': resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/sinonjs__fake-timers@8.1.1': resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} '@types/sizzle@2.3.10': resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/swagger-schema-official@2.0.25': resolution: {integrity: sha512-T92Xav+Gf/Ik1uPW581nA+JftmjWPgskw/WBf4TJzxRG/SJ+DfNnNE+WuZ4mrXuzflQMqMkm1LSYjzYW7MB1Cg==} @@ -2610,6 +3244,12 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -2672,6 +3312,9 @@ packages: resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@unocss/astro@66.5.4': resolution: {integrity: sha512-6KsilC1SiTBmEJRMuPl+Mg8KDWB1+DaVoirGZR7BAEtMf2NzrfQcR4+O/3DHtzb38pfb0K1aHCfWwCozHxLlfA==} peerDependencies: @@ -2995,6 +3638,57 @@ packages: vue: optional: true + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -3002,11 +3696,25 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -3046,6 +3754,16 @@ packages: peerDependencies: ajv: ^8.0.0-beta.0 + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -3083,10 +3801,18 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + any-base@1.1.0: resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} @@ -3094,6 +3820,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + arch@2.2.0: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} @@ -3101,6 +3830,12 @@ packages: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -3111,6 +3846,9 @@ packages: array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -3119,6 +3857,9 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} @@ -3176,6 +3917,20 @@ packages: axios@1.12.2: resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + babel-jest@30.2.0: + resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} + + babel-plugin-jest-hoist@30.2.0: + resolution: {integrity: sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + babel-plugin-polyfill-corejs2@0.4.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: @@ -3191,6 +3946,17 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.2.0: + resolution: {integrity: sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3200,10 +3966,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.25: - resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==} - hasBin: true - baseline-browser-mapping@2.9.11: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true @@ -3215,6 +3977,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blob-util@2.0.2: resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} @@ -3224,6 +3989,10 @@ packages: bn.js@4.12.1: resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -3240,16 +4009,18 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - browserslist@4.27.0: - resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -3266,6 +4037,10 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + byline@5.0.0: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} @@ -3320,6 +4095,14 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + camelize-ts@3.0.0: resolution: {integrity: sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3349,9 +4132,16 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -3368,6 +4158,10 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} @@ -3379,6 +4173,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -3395,10 +4192,18 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + cli-table3@0.6.1: resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} engines: {node: 10.* || >= 12.*} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-truncate@2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} @@ -3407,6 +4212,10 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + clipboard@2.0.11: resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==} @@ -3414,6 +4223,17 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -3448,10 +4268,18 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} + comment-json@4.4.1: + resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} + engines: {node: '>= 6'} + comment-parser@1.4.1: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} @@ -3463,9 +4291,16 @@ packages: compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -3480,6 +4315,14 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} engines: {node: '>=16'} @@ -3504,6 +4347,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + core-js-compat@3.46.0: resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} @@ -3516,6 +4362,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cosmiconfig-typescript-loader@6.2.0: resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} engines: {node: '>=v18'} @@ -3524,6 +4374,15 @@ packages: cosmiconfig: '>=9' typescript: '>=5' + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + cosmiconfig@9.0.0: resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} @@ -3533,6 +4392,9 @@ packages: typescript: optional: true + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cron-validator@1.4.0: resolution: {integrity: sha512-wGcJ9FCy65iaU6egSH8b5dZYJF7GU/3Jh06wzaT9lsa5dbqExjljmu+0cJ8cpKn+vUyZa/EM4WAxeLR6SypJXw==} @@ -3637,6 +4499,14 @@ packages: decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deeks@3.1.0: resolution: {integrity: sha512-e7oWH1LzIdv/prMQ7pmlDlaVoL64glqzvNgkgQNgyec9ORPHrT2jaOqMtRyqJuwWjtfb6v+2rk9pmaHj+F137A==} engines: {node: '>= 16'} @@ -3656,6 +4526,9 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3685,13 +4558,24 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + diff-sequences@27.5.1: resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3748,6 +4632,9 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@3.18.4: resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} @@ -3756,15 +4643,16 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.245: - resolution: {integrity: sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==} - electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3778,6 +4666,10 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -3863,6 +4755,10 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -3888,6 +4784,12 @@ packages: peerDependencies: eslint: ^9.5.0 + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + eslint-flat-config-utils@1.1.0: resolution: {integrity: sha512-W49wz7yQJGRfg4QSV3nwdO/fYcWetiSKhLV5YykfQMcqnIATNpoS7EPdINhLB9P3fmdjNmFtOgZjiKnCndWAnw==} @@ -3981,6 +4883,20 @@ packages: peerDependencies: eslint: '>=8.45.0' + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-plugin-regexp@2.10.0: resolution: {integrity: sha512-ovzQT8ESVn5oOe5a7gIDPD5v9bCSjIFJu57sVPDqgPRXicQzOnYfFN21WoQBQF18vrhT5o7UMKFwJQVVjyJ0ng==} engines: {node: ^18 || >=20} @@ -4026,6 +4942,10 @@ packages: '@vue/compiler-sfc': ^3.3.0 eslint: ^8.50.0 || ^9.0.0 + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4060,6 +4980,11 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -4068,6 +4993,10 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -4089,16 +5018,28 @@ packages: resolution: {integrity: sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==} engines: {node: '>=6.0.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter2@6.4.7: resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -4107,10 +5048,22 @@ packages: resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} engines: {node: '>=4'} + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + expect@30.2.0: + resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -4142,6 +5095,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -4203,6 +5159,9 @@ packages: fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -4226,6 +5185,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@21.2.0: + resolution: {integrity: sha512-vCYBgFOrJQLoTzDyAXAL/RFfKnXXpUYt4+tipVy26nJJhT7ftgGETf2tAQF59EEL61i3MrorV/PG6tf7LJK7eg==} + engines: {node: '>=20'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -4233,6 +5196,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-my-way@8.2.2: resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} engines: {node: '>=14'} @@ -4288,6 +5255,13 @@ packages: forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + form-data@2.3.3: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} engines: {node: '>= 0.12'} @@ -4296,10 +5270,18 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -4307,10 +5289,21 @@ packages: fp-ts@2.16.9: resolution: {integrity: sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ==} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -4357,6 +5350,10 @@ packages: get-own-enumerable-property-symbols@3.0.2: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -4365,6 +5362,10 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -4399,6 +5400,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -4408,6 +5412,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + glob@13.0.0: + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} + engines: {node: 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -4448,6 +5456,10 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -4483,6 +5495,11 @@ packages: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + har-schema@2.0.0: resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} engines: {node: '>=4'} @@ -4568,6 +5585,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -4591,6 +5612,10 @@ packages: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -4604,6 +5629,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + engines: {node: '>=0.10.0'} + idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} @@ -4628,6 +5657,11 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -4745,6 +5779,10 @@ packages: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + is-generator-function@1.1.2: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} @@ -4757,6 +5795,10 @@ packages: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -4795,6 +5837,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4883,6 +5928,10 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} @@ -4895,6 +5944,10 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -4910,6 +5963,138 @@ packages: javascript-time-ago@2.5.12: resolution: {integrity: sha512-s8PPq2HQ3HIbSU0SjhNvTitf5VoXbQWof9q6k3gIX7F2il0ptjD5lONTDccpuKt/2U7RjbCp/TCHPK7eDwO7zQ==} + jest-changed-files@30.2.0: + resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.2.0: + resolution: {integrity: sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.2.0: + resolution: {integrity: sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.2.0: + resolution: {integrity: sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.2.0: + resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.2.0: + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.2.0: + resolution: {integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@30.2.0: + resolution: {integrity: sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-haste-map@30.2.0: + resolution: {integrity: sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.2.0: + resolution: {integrity: sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.2.0: + resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@30.2.0: + resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@30.2.0: + resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.2.0: + resolution: {integrity: sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.2.0: + resolution: {integrity: sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.2.0: + resolution: {integrity: sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.2.0: + resolution: {integrity: sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.2.0: + resolution: {integrity: sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.2.0: + resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.2.0: + resolution: {integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.2.0: + resolution: {integrity: sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@30.2.0: + resolution: {integrity: sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.2.0: + resolution: {integrity: sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -4927,6 +6112,10 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -5008,6 +6197,9 @@ packages: resolution: {integrity: sha512-uuPNLJkKN8NXAlZlQ6kmUF9qO+T6Kyd7oV4+/7yy8Jz6+MZNyhPq8EdLpdfnPVzUC8qSf1b4j1azKaGnFsjmsw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -5106,10 +6298,18 @@ packages: resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} engines: {node: '>=18.0.0'} + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + engines: {node: '>=6.11.5'} + local-pkg@0.5.1: resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} engines: {node: '>=14'} @@ -5142,6 +6342,9 @@ packages: lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -5203,6 +6406,9 @@ packages: magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -5213,6 +6419,12 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -5262,6 +6474,18 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -5274,6 +6498,10 @@ packages: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5281,6 +6509,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -5376,10 +6608,23 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -5433,6 +6678,10 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -5452,10 +6701,18 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + multer@2.0.2: + resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} + engines: {node: '>= 10.16.0'} + mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -5487,9 +6744,22 @@ packages: resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + node-fetch-h2@2.3.0: resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==} engines: {node: 4.x || >=6.0.0} @@ -5506,6 +6776,9 @@ packages: encoding: optional: true + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-readfiles@0.2.0: resolution: {integrity: sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==} @@ -5570,6 +6843,10 @@ packages: oauth4webapi@3.8.2: resolution: {integrity: sha512-FzZZ+bht5X0FKe7Mwz3DAVAmlH1BV5blSak/lHMBKz0/EBMhX6B10GlQYI51+oRp8ObJaX0g6pXrAxZh5s8rjw==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -5595,6 +6872,10 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -5623,6 +6904,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} @@ -5700,6 +6985,10 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -5738,6 +7027,9 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -5779,6 +7071,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -5824,6 +7120,14 @@ packages: resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} hasBin: true + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -5886,6 +7190,15 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} @@ -5894,6 +7207,10 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} + pretty-format@30.2.0: + resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + prisma@6.19.0: resolution: {integrity: sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==} engines: {node: '>=18.18'} @@ -5943,6 +7260,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + qified@0.5.1: resolution: {integrity: sha512-+BtFN3dCP+IaFA6IYNOu/f/uK1B8xD2QWyOeCse0rjtAebBmkzgd2d1OAXi3ikAzJMIBSdzZDNZ3wZKEUDQs5w==} engines: {node: '>=20'} @@ -5951,6 +7271,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} @@ -5974,9 +7298,17 @@ packages: randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + rate-limiter-flexible@4.0.1: resolution: {integrity: sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -5984,6 +7316,9 @@ packages: resolution: {integrity: sha512-VXUdgSiUrE/WZXn6gUIVVIsg0+Hp6VPZPOaHCay+OuFKy6u/8ktmeNEf+U5qSA8jzGGFsg8jrDNu1BeHpz2pJA==} engines: {node: '>=10'} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -5999,6 +7334,10 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -6015,6 +7354,9 @@ packages: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -6083,6 +7425,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -6140,6 +7486,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -6149,6 +7499,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -6184,6 +7537,14 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + schemes@1.4.0: resolution: {integrity: sha512-ImFy9FbCsQlVgnE3TCWmLPCFnVzx0lHL/l+umHplDqAKd0dzFpnS6lFZIpagBlYhKwzVmlV36ec0Y1XTu8JBAQ==} @@ -6216,9 +7577,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -6353,6 +7722,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -6360,6 +7732,14 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} @@ -6388,6 +7768,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} engines: {node: '>=0.10.0'} @@ -6397,6 +7780,10 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -6404,6 +7791,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -6418,10 +7809,18 @@ packages: resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} engines: {node: '>= 0.10.0'} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -6473,6 +7872,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + strip-comments@2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} @@ -6500,6 +7903,10 @@ packages: strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + stylelint-config-html@1.1.0: resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} engines: {node: ^12 || >=14} @@ -6537,6 +7944,14 @@ packages: engines: {node: '>=18.12.0'} hasBin: true + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + + supertest@7.2.1: + resolution: {integrity: sha512-/OfhUL9WRLfoovZuWJ4l+2GVz3Eoo8Eo2TZVs9QxF2kmxdrmK7rCww4iJBstHevUH/M44aJ9TMN7yB+W+oWxlA==} + engines: {node: '>=14.18.0'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -6572,6 +7987,10 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -6608,11 +8027,31 @@ packages: resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} engines: {node: '>=10'} + terser-webpack-plugin@5.3.16: + resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + terser@5.44.1: resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} engines: {node: '>=10'} hasBin: true + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -6670,6 +8109,9 @@ packages: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -6682,6 +8124,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + toml-eslint-parser@0.10.0: resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6742,10 +8188,66 @@ packages: typescript: optional: true + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-loader@9.5.4: + resolution: {integrity: sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + ts-patch@3.3.0: resolution: {integrity: sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg==} hasBin: true + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -6798,6 +8300,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + type-fest@0.16.0: resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} engines: {node: '>=10'} @@ -6818,6 +8324,18 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -6834,6 +8352,9 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript-eslint@8.46.3: resolution: {integrity: sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6859,6 +8380,19 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -6869,6 +8403,9 @@ packages: undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -6931,6 +8468,10 @@ packages: vite: optional: true + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin-auto-import@0.18.6: resolution: {integrity: sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==} engines: {node: '>=14'} @@ -6975,12 +8516,6 @@ packages: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -7014,9 +8549,20 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} @@ -7197,6 +8743,16 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.5.0: + resolution: {integrity: sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -7207,9 +8763,27 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + engines: {node: '>=10.13.0'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + webpack@5.103.0: + resolution: {integrity: sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -7270,6 +8844,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workbox-background-sync@7.3.0: resolution: {integrity: sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==} @@ -7411,6 +8988,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -7419,6 +9000,10 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + zod-validation-error@3.5.4: resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} engines: {node: '>=18.0.0'} @@ -7444,6 +9029,60 @@ snapshots: ts-deepmerge: 6.2.1 zod: 3.25.76 + '@angular-devkit/core@19.2.17(chokidar@4.0.3)': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + jsonc-parser: 3.3.1 + picomatch: 4.0.2 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.19(chokidar@4.0.3)': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + jsonc-parser: 3.3.1 + picomatch: 4.0.2 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.19(@types/node@22.19.3)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@22.19.3) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.17(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.19(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + '@antfu/eslint-config@3.16.0(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.23)(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@2.1.9(@types/node@24.10.0)(jsdom@25.0.1)(terser@5.44.1))': dependencies: '@antfu/install-pkg': 1.1.0 @@ -7716,126 +9355,206 @@ snapshots: dependencies: '@babel/core': 7.28.5 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.5)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.5)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.5)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.5)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/template': 7.27.2 - '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.28.5)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.5)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) - transitivePeerDependencies: - - supports-color + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color '@babel/plugin-transform-exponentiation-operator@7.28.5(@babel/core@7.28.5)': dependencies: @@ -8193,6 +9912,8 @@ snapshots: '@biomejs/wasm-nodejs@2.2.6': {} + '@borewit/text-codec@0.2.1': {} + '@cacheable/memoize@2.0.3': dependencies: '@cacheable/utils': 2.2.0 @@ -8219,6 +9940,9 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@colors/colors@1.5.0': + optional: true + '@commitlint/cli@19.8.1(@types/node@24.10.0)(typescript@5.9.3)': dependencies: '@commitlint/format': 19.8.1 @@ -8329,6 +10053,10 @@ snapshots: '@types/conventional-commits-parser': 5.0.2 chalk: 5.6.2 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -8801,6 +10529,146 @@ snapshots: '@iconify/types': 2.0.0 vue: 3.5.23(typescript@5.9.3) + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@22.19.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/confirm@5.1.21(@types/node@22.19.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/type': 3.0.10(@types/node@22.19.3) + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/core@10.3.2(@types/node@22.19.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.3) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/editor@4.2.23(@types/node@22.19.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/external-editor': 1.0.3(@types/node@22.19.3) + '@inquirer/type': 3.0.10(@types/node@22.19.3) + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/expand@4.0.23(@types/node@22.19.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/type': 3.0.10(@types/node@22.19.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/external-editor@1.0.3(@types/node@22.19.3)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.1 + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@22.19.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/type': 3.0.10(@types/node@22.19.3) + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/number@3.0.23(@types/node@22.19.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/type': 3.0.10(@types/node@22.19.3) + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/password@4.0.23(@types/node@22.19.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/type': 3.0.10(@types/node@22.19.3) + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/prompts@7.10.1(@types/node@22.19.3)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.3) + '@inquirer/confirm': 5.1.21(@types/node@22.19.3) + '@inquirer/editor': 4.2.23(@types/node@22.19.3) + '@inquirer/expand': 4.0.23(@types/node@22.19.3) + '@inquirer/input': 4.3.1(@types/node@22.19.3) + '@inquirer/number': 3.0.23(@types/node@22.19.3) + '@inquirer/password': 4.0.23(@types/node@22.19.3) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.3) + '@inquirer/search': 3.2.2(@types/node@22.19.3) + '@inquirer/select': 4.4.2(@types/node@22.19.3) + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/prompts@7.3.2(@types/node@22.19.3)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.3) + '@inquirer/confirm': 5.1.21(@types/node@22.19.3) + '@inquirer/editor': 4.2.23(@types/node@22.19.3) + '@inquirer/expand': 4.0.23(@types/node@22.19.3) + '@inquirer/input': 4.3.1(@types/node@22.19.3) + '@inquirer/number': 3.0.23(@types/node@22.19.3) + '@inquirer/password': 4.0.23(@types/node@22.19.3) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.3) + '@inquirer/search': 3.2.2(@types/node@22.19.3) + '@inquirer/select': 4.4.2(@types/node@22.19.3) + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/rawlist@4.1.11(@types/node@22.19.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/type': 3.0.10(@types/node@22.19.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/search@3.2.2(@types/node@22.19.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/select@4.4.2(@types/node@22.19.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.3 + + '@inquirer/type@3.0.10(@types/node@22.19.3)': + optionalDependencies: + '@types/node': 22.19.3 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -8820,8 +10688,195 @@ snapshots: dependencies: minipass: 7.1.2 + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + '@istanbuljs/schema@0.1.3': {} + '@jest/console@30.2.0': + dependencies: + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + chalk: 4.1.2 + jest-message-util: 30.2.0 + jest-util: 30.2.0 + slash: 3.0.0 + + '@jest/core@30.2.0(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.2.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-resolve-dependencies: 30.2.0 + jest-runner: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + jest-watcher: 30.2.0 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.0.1': {} + + '@jest/environment@30.2.0': + dependencies: + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + jest-mock: 30.2.0 + + '@jest/expect-utils@30.2.0': + dependencies: + '@jest/get-type': 30.1.0 + + '@jest/expect@30.2.0': + dependencies: + expect: 30.2.0 + jest-snapshot: 30.2.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@30.2.0': + dependencies: + '@jest/types': 30.2.0 + '@sinonjs/fake-timers': 13.0.5 + '@types/node': 22.19.3 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-util: 30.2.0 + + '@jest/get-type@30.1.0': {} + + '@jest/globals@30.2.0': + dependencies: + '@jest/environment': 30.2.0 + '@jest/expect': 30.2.0 + '@jest/types': 30.2.0 + jest-mock: 30.2.0 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 22.19.3 + jest-regex-util: 30.0.1 + + '@jest/reporters@30.2.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.19.3 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.2.0 + jest-util: 30.2.0 + jest-worker: 30.2.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.46 + + '@jest/snapshot-utils@30.2.0': + dependencies: + '@jest/types': 30.2.0 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.2.0': + dependencies: + '@jest/console': 30.2.0 + '@jest/types': 30.2.0 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@30.2.0': + dependencies: + '@jest/test-result': 30.2.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.2.0 + slash: 3.0.0 + + '@jest/transform@30.2.0': + dependencies: + '@babel/core': 7.28.5 + '@jest/types': 30.2.0 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 7.0.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.2.0 + jest-regex-util: 30.0.1 + jest-util: 30.2.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@30.2.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.3 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8846,6 +10901,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': dependencies: jsep: 1.4.0 @@ -8915,6 +10975,8 @@ snapshots: - bufferutil - utf-8-validate + '@lukeed/csprng@1.1.0': {} + '@lukeed/ms@2.0.2': {} '@napi-rs/wasm-runtime@0.2.12': @@ -8924,6 +10986,91 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nestjs/cli@11.0.14(@types/node@22.19.3)': + dependencies: + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.19(@types/node@22.19.3)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@22.19.3) + '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.103.0) + glob: 13.0.0 + node-emoji: 1.11.0 + ora: 5.4.1 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.9.3 + webpack: 5.103.0 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - '@types/node' + - esbuild + - uglify-js + - webpack-cli + + '@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.2.0 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + transitivePeerDependencies: + - supports-color + + '@nestjs/core@11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.3.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + + '@nestjs/platform-express@11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + dependencies: + '@nestjs/common': 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.5 + express: 5.2.1 + multer: 2.0.2 + path-to-regexp: 8.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': + dependencies: + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.17(chokidar@4.0.3) + comment-json: 4.4.1 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - chokidar + + '@nestjs/testing@11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11)': + dependencies: + '@nestjs/common': 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8936,6 +11083,14 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@nuxt/opencollective@0.4.1': + dependencies: + consola: 3.4.2 + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -8990,12 +11145,14 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.29': {} - '@rollup/plugin-babel@5.3.1(@babel/core@7.28.5)(rollup@2.79.2)': + '@rollup/plugin-babel@5.3.1(@babel/core@7.28.5)(@types/babel__core@7.20.5)(rollup@2.79.2)': dependencies: '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 '@rollup/pluginutils': 3.1.0(rollup@2.79.2) rollup: 2.79.2 + optionalDependencies: + '@types/babel__core': 7.20.5 transitivePeerDependencies: - supports-color @@ -9104,6 +11261,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true + '@sinclair/typebox@0.34.46': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@13.0.5': + dependencies: + '@sinonjs/commons': 3.0.1 + '@standard-schema/spec@1.0.0': {} '@stylistic/eslint-plugin@2.13.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': @@ -9129,6 +11296,15 @@ snapshots: dependencies: tslib: 2.8.1 + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3(supports-color@5.5.0) + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + '@ts-rest/core@3.52.1(@types/node@24.10.0)(zod@3.25.76)': optionalDependencies: '@types/node': 24.10.0 @@ -9148,23 +11324,103 @@ snapshots: openapi3-ts: 2.0.2 zod: 3.25.76 + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.19.3 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.19.3 + '@types/conventional-commits-parser@5.0.2': dependencies: '@types/node': 24.10.0 + '@types/cookiejar@2.1.5': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@types/estree@0.0.39': {} '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.0': + dependencies: + '@types/node': 22.19.3 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.0 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@30.0.0': + dependencies: + expect: 30.2.0 + pretty-format: 30.2.0 + '@types/js-yaml@4.0.9': {} '@types/jsdom@21.1.7': @@ -9181,22 +11437,55 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/methods@1.1.4': {} + '@types/ms@2.1.0': {} + '@types/node@22.19.3': + dependencies: + undici-types: 6.21.0 + '@types/node@24.10.0': dependencies: undici-types: 7.16.0 '@types/normalize-package-data@2.4.4': {} + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + '@types/resolve@1.20.2': {} + '@types/send@1.2.1': + dependencies: + '@types/node': 22.19.3 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.19.3 + '@types/sinonjs__fake-timers@8.1.1': optional: true '@types/sizzle@2.3.10': optional: true + '@types/stack-utils@2.0.3': {} + + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.19.3 + form-data: 4.0.4 + + '@types/supertest@6.0.3': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + '@types/swagger-schema-official@2.0.25': {} '@types/tmp@0.2.6': @@ -9208,6 +11497,12 @@ snapshots: '@types/unist@3.0.3': {} + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + '@types/yauzl@2.10.3': dependencies: '@types/node': 24.10.0 @@ -9306,6 +11601,8 @@ snapshots: '@typescript-eslint/types': 8.46.3 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@unocss/astro@66.5.4(vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1))': dependencies: '@unocss/core': 66.5.4 @@ -9706,6 +12003,86 @@ snapshots: typescript: 5.9.3 vue: 3.5.23(typescript@5.9.3) + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -9713,10 +12090,23 @@ snapshots: abstract-logging@2.0.1: {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-phases@1.0.4(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + acorn@8.15.0: {} agent-base@7.1.4: {} @@ -9747,6 +12137,15 @@ snapshots: dependencies: ajv: 8.17.1 + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -9763,13 +12162,11 @@ snapshots: alien-signals@1.0.13: {} - ansi-colors@4.1.3: - optional: true + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 - optional: true ansi-escapes@7.2.0: dependencies: @@ -9787,8 +12184,12 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} + ansis@4.2.0: {} + any-base@1.1.0: {} anymatch@3.1.3: @@ -9796,11 +12197,19 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + append-field@1.0.0: {} + arch@2.2.0: optional: true are-docs-informative@0.0.2: {} + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} array-buffer-byte-length@1.0.2: @@ -9810,6 +12219,8 @@ snapshots: array-ify@1.0.0: {} + array-timsort@1.0.3: {} + array-union@2.1.0: {} arraybuffer.prototype.slice@1.0.4: @@ -9822,6 +12233,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + asap@2.0.6: {} + asn1.js@5.4.1: dependencies: bn.js: 4.12.1 @@ -9875,6 +12288,33 @@ snapshots: transitivePeerDependencies: - debug + babel-jest@30.2.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.2.0: + dependencies: + '@types/babel__core': 7.20.5 + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.5): dependencies: '@babel/compat-data': 7.28.5 @@ -9899,14 +12339,36 @@ snapshots: transitivePeerDependencies: - supports-color + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.5) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.5) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) + + babel-preset-jest@30.2.0(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + babel-plugin-jest-hoist: 30.2.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + balanced-match@1.0.2: {} balanced-match@2.0.0: {} - base64-js@1.5.1: - optional: true - - baseline-browser-mapping@2.8.25: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.9.11: {} @@ -9916,6 +12378,12 @@ snapshots: binary-extensions@2.3.0: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + blob-util@2.0.2: optional: true @@ -9924,6 +12392,20 @@ snapshots: bn.js@4.12.1: {} + body-parser@2.2.1: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@5.5.0) + http-errors: 2.0.0 + iconv-lite: 0.7.1 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -9942,14 +12424,6 @@ snapshots: brorand@1.1.0: optional: true - browserslist@4.27.0: - dependencies: - baseline-browser-mapping: 2.8.25 - caniuse-lite: 1.0.30001762 - electron-to-chromium: 1.5.245 - node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.27.0) - browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.11 @@ -9958,6 +12432,14 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + buffer-crc32@0.2.13: optional: true @@ -9970,10 +12452,13 @@ snapshots: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - optional: true builtin-modules@3.3.0: {} + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + byline@5.0.0: {} bytes@3.1.2: {} @@ -10047,6 +12532,10 @@ snapshots: callsites@3.1.0: {} + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + camelize-ts@3.0.0: {} caniuse-lite@1.0.30001762: {} @@ -10076,8 +12565,12 @@ snapshots: chalk@5.6.2: {} + char-regex@1.0.2: {} + character-entities@2.0.2: {} + chardet@2.1.1: {} + check-error@2.1.1: {} chokidar@3.6.0: @@ -10098,6 +12591,8 @@ snapshots: chownr@3.0.0: {} + chrome-trace-event@1.0.4: {} + ci-info@4.3.1: {} cidr-regex@3.1.1: @@ -10108,6 +12603,8 @@ snapshots: dependencies: consola: 3.4.2 + cjs-module-lexer@2.2.0: {} + clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -10118,12 +12615,13 @@ snapshots: cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 - optional: true cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 + cli-spinners@2.9.2: {} + cli-table3@0.6.1: dependencies: string-width: 4.2.3 @@ -10131,6 +12629,12 @@ snapshots: colors: 1.4.0 optional: true + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 @@ -10142,6 +12646,8 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 + cli-width@4.1.0: {} + clipboard@2.0.11: dependencies: good-listener: 1.2.2 @@ -10154,6 +12660,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@1.0.4: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -10181,9 +12693,17 @@ snapshots: commander@2.20.3: {} + commander@4.1.1: {} + commander@6.2.1: optional: true + comment-json@4.4.1: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + comment-parser@1.4.1: {} common-tags@1.8.2: {} @@ -10193,8 +12713,17 @@ snapshots: array-ify: 1.0.0 dot-prop: 5.3.0 + component-emitter@1.3.1: {} + concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + confbox@0.1.8: {} confbox@0.2.2: {} @@ -10205,6 +12734,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + conventional-changelog-angular@7.0.0: dependencies: compare-func: 2.0.0 @@ -10226,9 +12759,11 @@ snapshots: cookie@0.7.2: {} + cookiejar@2.1.4: {} + core-js-compat@3.46.0: dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 core-js-compat@3.47.0: dependencies: @@ -10238,6 +12773,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig-typescript-loader@6.2.0(@types/node@24.10.0)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): dependencies: '@types/node': 24.10.0 @@ -10245,6 +12785,15 @@ snapshots: jiti: 2.6.1 typescript: 5.9.3 + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -10254,6 +12803,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + create-require@1.1.1: {} + cron-validator@1.4.0: {} cross-spawn@6.0.6: @@ -10408,6 +12959,8 @@ snapshots: dependencies: character-entities: 2.0.2 + dedent@1.7.1: {} + deeks@3.1.0: {} deep-eql@5.0.2: {} @@ -10418,6 +12971,10 @@ snapshots: deepmerge@4.3.1: {} + defaults@1.0.4: + dependencies: + clone: 1.0.4 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -10442,12 +12999,21 @@ snapshots: destr@2.0.5: {} + detect-newline@3.1.0: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + diff-sequences@27.5.1: {} + diff@4.0.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -10503,6 +13069,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + ee-first@1.1.1: {} + effect@3.18.4: dependencies: '@standard-schema/spec': 1.0.0 @@ -10512,8 +13080,6 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.245: {} - electron-to-chromium@1.5.267: {} elliptic@6.6.1: @@ -10527,6 +13093,8 @@ snapshots: minimalistic-crypto-utils: 1.0.1 optional: true + emittery@0.13.1: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -10535,6 +13103,8 @@ snapshots: empathic@2.0.0: {} + encodeurl@2.0.0: {} + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -10762,6 +13332,8 @@ snapshots: escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -10781,6 +13353,10 @@ snapshots: '@eslint/compat': 1.4.1(eslint@9.39.1(jiti@2.6.1)) eslint: 9.39.1(jiti@2.6.1) + eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + eslint-flat-config-utils@1.1.0: dependencies: pathe: 2.0.3 @@ -10909,6 +13485,16 @@ snapshots: - supports-color - typescript + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + prettier: 3.7.4 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.11 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-regexp@2.10.0(eslint@9.39.1(jiti@2.6.1)): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -10987,6 +13573,11 @@ snapshots: '@vue/compiler-sfc': 3.5.23 eslint: 9.39.1(jiti@2.6.1) + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 @@ -11054,6 +13645,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -11062,6 +13655,8 @@ snapshots: dependencies: estraverse: 5.3.0 + estraverse@4.3.0: {} + estraverse@5.3.0: {} estree-walker@1.0.1: {} @@ -11076,11 +13671,15 @@ snapshots: eta@3.5.0: {} + etag@1.8.1: {} + eventemitter2@6.4.7: optional: true eventemitter3@5.0.1: {} + events@3.3.0: {} + execa@4.1.0: dependencies: cross-spawn: 7.0.6 @@ -11094,6 +13693,18 @@ snapshots: strip-final-newline: 2.0.0 optional: true + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -11111,8 +13722,52 @@ snapshots: pify: 2.3.0 optional: true + exit-x@0.2.2: {} + expect-type@1.2.2: {} + expect@30.2.0: + dependencies: + '@jest/expect-utils': 30.2.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.2.0 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-util: 30.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.1 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} extend@3.0.2: {} @@ -11142,6 +13797,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -11244,6 +13901,10 @@ snapshots: dependencies: format: 0.2.2 + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -11266,6 +13927,15 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@21.2.0: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.4 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -11274,6 +13944,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-my-way@8.2.2: dependencies: fast-deep-equal: 3.1.3 @@ -11331,6 +14012,23 @@ snapshots: forever-agent@0.6.1: {} + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.103.0): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.9.3 + webpack: 5.103.0 + form-data@2.3.3: dependencies: asynckit: 0.4.0 @@ -11345,12 +14043,34 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + format@0.2.2: {} + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + forwarded@0.2.0: {} fp-ts@2.16.9: {} + fresh@2.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 @@ -11358,6 +14078,8 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-monkey@1.1.0: {} + fs.realpath@1.0.0: {} fsevents@2.3.2: @@ -11402,6 +14124,8 @@ snapshots: get-own-enumerable-property-symbols@3.0.2: {} + get-package-type@0.1.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -11412,6 +14136,8 @@ snapshots: pump: 3.0.3 optional: true + get-stream@6.0.1: {} + get-stream@8.0.1: {} get-symbol-description@1.1.0: @@ -11453,6 +14179,8 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + glob@10.4.5: dependencies: foreground-child: 3.3.1 @@ -11471,6 +14199,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.0 + glob@13.0.0: + dependencies: + minimatch: 10.1.1 + minipass: 7.1.2 + path-scurry: 2.0.0 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -11515,6 +14249,8 @@ snapshots: globals@15.15.0: {} + globals@16.5.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -11558,6 +14294,15 @@ snapshots: dependencies: duplexer: 0.1.2 + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + har-schema@2.0.0: {} har-validator@5.1.5: @@ -11641,6 +14386,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -11673,6 +14426,8 @@ snapshots: human-signals@1.1.1: optional: true + human-signals@2.1.0: {} + human-signals@5.0.0: {} husky@9.1.7: {} @@ -11681,10 +14436,13 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.1: + dependencies: + safer-buffer: 2.1.2 + idb@7.1.1: {} - ieee754@1.2.1: - optional: true + ieee754@1.2.1: {} ignore-by-default@1.0.1: {} @@ -11699,6 +14457,11 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} @@ -11803,6 +14566,8 @@ snapshots: dependencies: get-east-asian-width: 1.4.0 + is-generator-fn@2.1.0: {} + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -11821,6 +14586,8 @@ snapshots: is-path-inside: 3.0.3 optional: true + is-interactive@1.0.0: {} + is-map@2.0.3: {} is-module@1.0.0: {} @@ -11845,6 +14612,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -11887,8 +14656,7 @@ snapshots: is-typedarray@1.0.0: {} - is-unicode-supported@0.1.0: - optional: true + is-unicode-supported@0.1.0: {} is-weakmap@2.0.2: {} @@ -11917,6 +14685,16 @@ snapshots: istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 @@ -11936,6 +14714,8 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + iterare@1.2.1: {} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -11946,15 +14726,333 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 - jake@10.9.4: + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.4 + picocolors: 1.1.1 + + javascript-time-ago@2.5.12: + dependencies: + relative-time-format: 1.1.11 + + jest-changed-files@30.2.0: + dependencies: + execa: 5.1.1 + jest-util: 30.2.0 + p-limit: 3.1.0 + + jest-circus@30.2.0: + dependencies: + '@jest/environment': 30.2.0 + '@jest/expect': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.1 + is-generator-fn: 2.1.0 + jest-each: 30.2.0 + jest-matcher-utils: 30.2.0 + jest-message-util: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + p-limit: 3.1.0 + pretty-format: 30.2.0 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + jest-util: 30.2.0 + jest-validate: 30.2.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.3 + ts-node: 10.9.2(@types/node@22.19.3)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.2.0: + dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.2.0 + + jest-docblock@30.2.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.2.0 + chalk: 4.1.2 + jest-util: 30.2.0 + pretty-format: 30.2.0 + + jest-environment-node@30.2.0: + dependencies: + '@jest/environment': 30.2.0 + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + jest-mock: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + + jest-haste-map@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.2.0 + jest-worker: 30.2.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.2.0 + + jest-matcher-utils@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.2.0 + pretty-format: 30.2.0 + + jest-message-util@30.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 30.2.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + jest-util: 30.2.0 + + jest-pnp-resolver@1.2.3(jest-resolve@30.2.0): + optionalDependencies: + jest-resolve: 30.2.0 + + jest-regex-util@30.0.1: {} + + jest-resolve-dependencies@30.2.0: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.2.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.2.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.2.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.2.0) + jest-util: 30.2.0 + jest-validate: 30.2.0 + slash: 3.0.0 + unrs-resolver: 1.11.1 + + jest-runner@30.2.0: + dependencies: + '@jest/console': 30.2.0 + '@jest/environment': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-haste-map: 30.2.0 + jest-leak-detector: 30.2.0 + jest-message-util: 30.2.0 + jest-resolve: 30.2.0 + jest-runtime: 30.2.0 + jest-util: 30.2.0 + jest-watcher: 30.2.0 + jest-worker: 30.2.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.2.0: + dependencies: + '@jest/environment': 30.2.0 + '@jest/fake-timers': 30.2.0 + '@jest/globals': 30.2.0 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + chalk: 4.1.2 + cjs-module-lexer: 2.2.0 + collect-v8-coverage: 1.0.3 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.2.0: + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/types': 7.28.5 + '@jest/expect-utils': 30.2.0 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + expect: 30.2.0 + graceful-fs: 4.2.11 + jest-diff: 30.2.0 + jest-matcher-utils: 30.2.0 + jest-message-util: 30.2.0 + jest-util: 30.2.0 + pretty-format: 30.2.0 + semver: 7.7.3 + synckit: 0.11.11 + transitivePeerDependencies: + - supports-color + + jest-util@30.2.0: + dependencies: + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + chalk: 4.1.2 + ci-info: 4.3.1 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + + jest-validate@30.2.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.2.0 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.2.0 + + jest-watcher@30.2.0: + dependencies: + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.3 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.2.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 22.19.3 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@30.2.0: dependencies: - async: 3.2.6 - filelist: 1.0.4 - picocolors: 1.1.1 + '@types/node': 22.19.3 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.2.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 - javascript-time-ago@2.5.12: + jest@30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: - relative-time-format: 1.1.11 + '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + '@jest/types': 30.2.0 + import-local: 3.2.0 + jest-cli: 30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node jiti@2.6.1: {} @@ -11967,6 +15065,11 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -12053,6 +15156,8 @@ snapshots: espree: 9.6.1 semver: 7.7.3 + jsonc-parser@3.3.1: {} + jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -12193,6 +15298,8 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 + load-esm@1.0.3: {} + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 @@ -12200,6 +15307,8 @@ snapshots: pify: 3.0.0 strip-bom: 3.0.0 + loader-runner@4.3.1: {} + local-pkg@0.5.1: dependencies: mlly: 1.8.0 @@ -12231,6 +15340,8 @@ snapshots: lodash.kebabcase@4.1.1: {} + lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} @@ -12256,7 +15367,6 @@ snapshots: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - optional: true log-update@4.0.0: dependencies: @@ -12290,6 +15400,10 @@ snapshots: dependencies: sourcemap-codec: 1.4.8 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -12304,6 +15418,12 @@ snapshots: dependencies: semver: 7.7.3 + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + markdown-table@3.0.4: {} math-intrinsics@1.1.0: {} @@ -12425,16 +15545,28 @@ snapshots: mdn-data@2.12.2: {} + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + memorystream@0.3.1: {} meow@12.1.1: {} meow@13.2.0: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} + methods@1.1.2: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -12640,14 +15772,21 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + mime@3.0.0: {} - mimic-fn@2.1.0: - optional: true + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -12684,6 +15823,10 @@ snapshots: dependencies: minipass: 7.1.2 + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -12703,8 +15846,20 @@ snapshots: muggle-string@0.4.1: {} + multer@2.0.2: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + mustache@4.2.0: {} + mute-stream@2.0.0: {} + nanoid@3.3.11: {} nanoid@5.0.9: {} @@ -12724,8 +15879,18 @@ snapshots: railroad-diagrams: 1.0.0 randexp: 0.4.6 + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + nice-try@1.0.5: {} + node-abort-controller@3.1.1: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.17.21 + node-fetch-h2@2.3.0: dependencies: http2-client: 1.3.5 @@ -12736,6 +15901,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-int64@0.4.0: {} + node-readfiles@0.2.0: dependencies: es6-promise: 3.3.1 @@ -12779,7 +15946,6 @@ snapshots: npm-run-path@4.0.1: dependencies: path-key: 3.1.1 - optional: true npm-run-path@5.3.0: dependencies: @@ -12835,6 +16001,8 @@ snapshots: oauth4webapi@3.8.2: optional: true + object-assign@4.1.1: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -12860,6 +16028,10 @@ snapshots: on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -12867,7 +16039,6 @@ snapshots: onetime@5.1.2: dependencies: mimic-fn: 2.1.0 - optional: true onetime@6.0.0: dependencies: @@ -12898,6 +16069,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + ospath@1.2.2: optional: true @@ -12974,6 +16157,8 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -13000,6 +16185,8 @@ snapshots: lru-cache: 11.2.2 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + path-type@3.0.0: dependencies: pify: 3.0.0 @@ -13027,6 +16214,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.2: {} + picomatch@4.0.3: {} pidtree@0.3.1: {} @@ -13084,6 +16273,12 @@ snapshots: sonic-boom: 4.2.0 thread-stream: 3.1.0 + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -13145,10 +16340,22 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.7.4: {} + pretty-bytes@5.6.0: {} pretty-bytes@6.1.1: {} + pretty-format@30.2.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3): dependencies: '@prisma/config': 6.19.0(magicast@0.3.5) @@ -13192,6 +16399,8 @@ snapshots: pure-rand@6.1.0: {} + pure-rand@7.0.1: {} + qified@0.5.1: dependencies: hookified: 1.12.2 @@ -13200,6 +16409,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + qs@6.5.3: {} quansync@0.2.11: {} @@ -13219,8 +16432,17 @@ snapshots: dependencies: safe-buffer: 5.2.1 + range-parser@1.2.1: {} + rate-limiter-flexible@4.0.1: {} + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + unpipe: 1.0.0 + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -13229,6 +16451,8 @@ snapshots: re2-wasm@1.0.2: optional: true + react-is@18.3.1: {} + read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 @@ -13258,6 +16482,12 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -13270,6 +16500,8 @@ snapshots: dependencies: '@eslint-community/regexpp': 4.12.2 + reflect-metadata@0.2.2: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -13366,6 +16598,10 @@ snapshots: require-from-string@2.0.2: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -13382,7 +16618,6 @@ snapshots: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - optional: true restore-cursor@5.1.0: dependencies: @@ -13436,6 +16671,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.7.1: {} rrweb-cssom@0.8.0: {} @@ -13444,10 +16689,13 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + rxjs@7.8.2: dependencies: tslib: 2.8.1 - optional: true safe-array-concat@1.1.3: dependencies: @@ -13484,6 +16732,19 @@ snapshots: dependencies: xmlchars: 2.2.0 + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + schemes@1.4.0: dependencies: extend: 3.0.2 @@ -13508,10 +16769,35 @@ snapshots: semver@7.7.3: {} + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -13615,8 +16901,7 @@ snapshots: siginfo@2.0.0: {} - signal-exit@3.0.7: - optional: true + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -13669,6 +16954,11 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -13676,6 +16966,10 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.4: {} + + source-map@0.7.6: {} + source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 @@ -13703,6 +16997,8 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.0.3: {} + sshpk@1.18.0: dependencies: asn1: 0.2.6 @@ -13717,10 +17013,16 @@ snapshots: stable-hash-x@0.2.0: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + stackback@0.0.2: {} statuses@2.0.1: {} + statuses@2.0.2: {} + std-env@3.10.0: {} steed@1.1.3: @@ -13738,8 +17040,15 @@ snapshots: stream-buffers@3.0.3: {} + streamsearch@1.1.0: {} + string-argv@0.3.2: {} + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -13824,10 +17133,11 @@ snapshots: strip-bom@3.0.0: {} + strip-bom@4.0.0: {} + strip-comments@2.0.1: {} - strip-final-newline@2.0.0: - optional: true + strip-final-newline@2.0.0: {} strip-final-newline@3.0.0: {} @@ -13843,6 +17153,10 @@ snapshots: dependencies: js-tokens: 9.0.1 + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@16.25.0(typescript@5.9.3)): dependencies: postcss-html: 1.8.0 @@ -13913,6 +17227,28 @@ snapshots: - supports-color - typescript + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@5.5.0) + fast-safe-stringify: 2.1.1 + form-data: 4.0.5 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.1 + transitivePeerDependencies: + - supports-color + + supertest@7.2.1: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -13924,7 +17260,6 @@ snapshots: supports-color@8.1.1: dependencies: has-flag: 4.0.0 - optional: true supports-hyperlinks@3.2.0: dependencies: @@ -13976,6 +17311,8 @@ snapshots: transitivePeerDependencies: - encoding + symbol-observable@4.0.0: {} + symbol-tree@3.2.4: {} synckit@0.11.11: @@ -14014,6 +17351,15 @@ snapshots: type-fest: 0.16.0 unique-string: 2.0.0 + terser-webpack-plugin@5.3.16(webpack@5.103.0): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.103.0 + terser@5.44.1: dependencies: '@jridgewell/source-map': 0.3.11 @@ -14021,6 +17367,12 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -14066,6 +17418,8 @@ snapshots: tmp@0.2.5: optional: true + tmpl@1.0.5: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -14074,6 +17428,12 @@ snapshots: toidentifier@1.0.1: {} + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.1 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + toml-eslint-parser@0.10.0: dependencies: eslint-visitor-keys: 3.4.3 @@ -14121,6 +17481,54 @@ snapshots: optionalDependencies: typescript: 5.9.3 + ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + jest-util: 30.2.0 + + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.103.0): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 + semver: 7.7.3 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.103.0 + + ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.19.3 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + ts-patch@3.3.0: dependencies: chalk: 4.1.2 @@ -14130,6 +17538,19 @@ snapshots: semver: 7.7.3 strip-ansi: 6.0.1 + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + tapable: 2.3.0 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@2.8.1: {} tsx@4.19.3: @@ -14177,17 +17598,31 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-detect@4.0.8: {} + type-fest@0.16.0: {} type-fest@0.20.2: {} - type-fest@0.21.3: - optional: true + type-fest@0.21.3: {} type-fest@0.6.0: {} type-fest@0.8.1: {} + type-fest@4.41.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -14221,6 +17656,8 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typedarray@0.0.6: {} + typescript-eslint@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -14243,6 +17680,15 @@ snapshots: ufo@1.6.1: {} + uglify-js@3.19.3: + optional: true + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -14259,6 +17705,8 @@ snapshots: undefsafe@2.0.5: {} + undici-types@6.21.0: {} + undici-types@7.16.0: {} undici@7.16.0: {} @@ -14347,6 +17795,8 @@ snapshots: - postcss - supports-color + unpipe@1.0.0: {} + unplugin-auto-import@0.18.6(rollup@2.79.2): dependencies: '@antfu/utils': 0.7.10 @@ -14418,12 +17868,6 @@ snapshots: upath@1.2.0: {} - update-browserslist-db@1.1.4(browserslist@4.27.0): - dependencies: - browserslist: 4.27.0 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -14446,11 +17890,21 @@ snapshots: uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vary@1.1.2: {} + verror@1.10.0: dependencies: assert-plus: 1.0.0 @@ -14475,13 +17929,13 @@ snapshots: - supports-color - terser - vite-plugin-pwa@1.1.0(vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1))(workbox-build@7.3.0)(workbox-window@7.3.0): + vite-plugin-pwa@1.1.0(vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): dependencies: debug: 4.4.3(supports-color@5.5.0) pretty-bytes: 6.1.1 tinyglobby: 0.2.15 vite: 7.2.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1) - workbox-build: 7.3.0 + workbox-build: 7.3.0(@types/babel__core@7.20.5) workbox-window: 7.3.0 transitivePeerDependencies: - supports-color @@ -14617,14 +18071,63 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.5.0: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + webidl-conversions@3.0.1: {} webidl-conversions@4.0.2: {} webidl-conversions@7.0.0: {} + webpack-node-externals@3.0.0: {} + + webpack-sources@3.3.3: {} + webpack-virtual-modules@0.6.2: {} + webpack@5.103.0: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.16(webpack@5.103.0) + watchpack: 2.5.0 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -14709,6 +18212,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + workbox-background-sync@7.3.0: dependencies: idb: 7.1.1 @@ -14718,13 +18223,13 @@ snapshots: dependencies: workbox-core: 7.3.0 - workbox-build@7.3.0: + workbox-build@7.3.0(@types/babel__core@7.20.5): dependencies: '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) '@babel/core': 7.28.5 '@babel/preset-env': 7.28.5(@babel/core@7.28.5) '@babel/runtime': 7.28.4 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.28.5)(rollup@2.79.2) + '@rollup/plugin-babel': 5.3.1(@babel/core@7.28.5)(@types/babel__core@7.20.5)(rollup@2.79.2) '@rollup/plugin-node-resolve': 15.3.1(rollup@2.79.2) '@rollup/plugin-replace': 2.4.2(rollup@2.79.2) '@rollup/plugin-terser': 0.4.4(rollup@2.79.2) @@ -14827,7 +18332,6 @@ snapshots: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - optional: true wrap-ansi@7.0.0: dependencies: @@ -14901,10 +18405,14 @@ snapshots: fd-slicer: 1.1.0 optional: true + yn@3.1.1: {} + yocto-queue@0.1.0: {} yocto-queue@1.2.1: {} + yoctocolors-cjs@2.1.3: {} + zod-validation-error@3.5.4(zod@3.25.76): dependencies: zod: 3.25.76 From 016c8217144e0c7fc32140318fb64bb8b28442db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Wed, 3 Dec 2025 15:36:16 +0100 Subject: [PATCH 02/33] chore: move the entirety of apps/server in server-nestjs --- apps/server-nestjs/.prettierrc | 12 +- apps/server-nestjs/package.json | 52 +- apps/server-nestjs/src/app.module.ts | 3 +- apps/server-nestjs/src/app.service.ts | 6 +- .../src/cpin-module/cpin.module.ts | 9 + .../cpin-module/cpin/cpin.controller.spec.ts | 18 + .../src/cpin-module/cpin/cpin.controller.ts | 4 + .../src/cpin-module/cpin/cpin.service.spec.ts | 18 + .../src/cpin-module/cpin/cpin.service.ts | 143 +++++ .../src/cpin-module/old-server/.env-example | 18 + .../old-server/.env.docker-example | 13 + .../cpin-module/old-server/.env.integ-example | 43 ++ .../src/cpin-module/old-server/Dockerfile | 66 +++ .../src/cpin-module/old-server/README.md | 39 ++ .../cpin-module/old-server/eslint.config.js | 3 + .../src/cpin-module/old-server/migrate-db.sh | 77 +++ .../src/cpin-module/old-server/nodemon.json | 4 + .../cpin-module/old-server/prisma.config.ts | 9 + .../old-server/src/__mocks__/prisma.ts | 14 + .../src/__mocks__/utils/hook-wrapper.ts | 34 ++ .../cpin-module/old-server/src/app.spec.ts | 21 + .../src/cpin-module/old-server/src/app.ts | 59 ++ .../old-server/src/connect.spec.ts | 61 +++ .../src/cpin-module/old-server/src/connect.ts | 52 ++ .../old-server/src/init/db/dump.ts | 28 + .../old-server/src/init/db/index.ts | 51 ++ .../old-server/src/init/db/utils.spec.ts | 52 ++ .../old-server/src/init/db/utils.ts | 85 +++ .../old-server/src/mocks/prisma.ts | 14 + .../cpin-module/old-server/src/mocks/utils.ts | 24 + .../src/cpin-module/old-server/src/plugins.ts | 46 ++ .../old-server/src/prepare-app.spec.ts | 70 +++ .../cpin-module/old-server/src/prepare-app.ts | 106 ++++ .../src/cpin-module/old-server/src/prisma.ts | 5 + .../20230706084346_dso/migration.sql | 151 +++++ .../20230710181052_dso/migration.sql | 85 +++ .../20230711132934_dso/migration.sql | 11 + .../20230802143822_dso/migration.sql | 10 + .../20230912084459_dso/migration.sql | 2 + .../20231010111515_dso/migration.sql | 8 + .../20231011125838_dso/migration.sql | 81 +++ .../20231011125839_dso/migration.sql | 35 ++ .../20231011125841_dso/migration.sql | 36 ++ .../20231012105520_dso/migration.sql | 11 + .../20231024155020_dso/migration.sql | 3 + .../20231026150220_dso/migration.sql | 3 + .../20240112135751_dso/migration.sql | 2 + .../20240321123436_dso/migration.sql | 12 + .../20240329172938_dso/migration.sql | 46 ++ .../20240424093852_dso/migration.sql | 23 + .../20240427181037_dso/migration.sql | 19 + .../20240605135052_dso/migration.sql | 9 + .../20240612123132_dso/migration.sql | 8 + .../20240614222908_dso/migration.sql | 11 + .../20240618112205_dso/migration.sql | 58 ++ .../20240717084709_dso/migration.sql | 9 + .../20240723135420_dso/migration.sql | 198 +++++++ .../20240725162050_dso/migration.sql | 5 + .../20240726210139_dso/migration.sql | 14 + .../20240808082632_dso/migration.sql | 17 + .../20240826143230_dso/migration.sql | 3 + .../20240829085548_dso/migration.sql | 12 + .../20240916141253_token/migration.sql | 23 + .../migration.sql | 8 + .../20240923142722_dso/migration.sql | 2 + .../20240923155416_dso/migration.sql | 2 + .../20240928002900_dso/migration.sql | 2 + .../migration.sql | 12 + .../20241104232540_add_usertype/migration.sql | 12 + .../20241104232541_add_pat/migration.sql | 84 +++ .../migration.sql | 2 + .../20241112101945_add_slug/migration.sql | 14 + .../migration.sql | 2 + .../20241216131342_dso/migration.sql | 17 + .../20250107104749_dso/migration.sql | 2 + .../migration.sql | 25 + .../migration.sql | 15 + .../20250723141246_dso/migration.sql | 2 + .../20250818095032_remove_quota/migration.sql | 44 ++ .../migration.sql | 5 + .../migration.sql | 9 + .../migration.sql | 4 + .../src/prisma/migrations/migration_lock.toml | 3 + .../old-server/src/prisma/schema/admin.prisma | 20 + .../src/prisma/schema/project.prisma | 103 ++++ .../src/prisma/schema/schema.prisma | 21 + .../old-server/src/prisma/schema/token.prisma | 30 + .../src/prisma/schema/topography.prisma | 53 ++ .../old-server/src/prisma/schema/user.prisma | 23 + .../src/resources/admin-role/business.spec.ts | 183 +++++++ .../src/resources/admin-role/business.ts | 90 +++ .../src/resources/admin-role/queries.ts | 32 ++ .../src/resources/admin-role/router.spec.ts | 181 ++++++ .../src/resources/admin-role/router.ts | 74 +++ .../resources/admin-token/business.spec.ts | 73 +++ .../src/resources/admin-token/business.ts | 68 +++ .../src/resources/admin-token/router.spec.ts | 161 ++++++ .../src/resources/admin-token/router.ts | 44 ++ .../src/resources/cluster/business.spec.ts | 173 ++++++ .../src/resources/cluster/business.ts | 230 ++++++++ .../src/resources/cluster/queries.ts | 312 +++++++++++ .../src/resources/cluster/router.spec.ts | 311 +++++++++++ .../src/resources/cluster/router.ts | 125 +++++ .../resources/environment/business.spec.ts | 353 ++++++++++++ .../src/resources/environment/business.ts | 300 ++++++++++ .../src/resources/environment/queries.ts | 98 ++++ .../src/resources/environment/router.spec.ts | 372 +++++++++++++ .../src/resources/environment/router.ts | 109 ++++ .../old-server/src/resources/index.ts | 49 ++ .../src/resources/log/business.spec.ts | 42 ++ .../old-server/src/resources/log/business.ts | 13 + .../old-server/src/resources/log/queries.ts | 57 ++ .../src/resources/log/router.spec.ts | 93 ++++ .../old-server/src/resources/log/router.ts | 32 ++ .../src/resources/project-member/business.ts | 60 ++ .../src/resources/project-member/queries.ts | 33 ++ .../resources/project-member/router.spec.ts | 294 ++++++++++ .../src/resources/project-member/router.ts | 82 +++ .../resources/project-role/business.spec.ts | 195 +++++++ .../src/resources/project-role/business.ts | 77 +++ .../src/resources/project-role/queries.ts | 54 ++ .../src/resources/project-role/router.spec.ts | 316 +++++++++++ .../src/resources/project-role/router.ts | 90 +++ .../src/resources/project-service/business.ts | 95 ++++ .../src/resources/project-service/queries.ts | 54 ++ .../resources/project-service/router.spec.ts | 160 ++++++ .../src/resources/project-service/router.ts | 38 ++ .../src/resources/project/business.spec.ts | 361 ++++++++++++ .../src/resources/project/business.ts | 261 +++++++++ .../src/resources/project/queries.ts | 335 ++++++++++++ .../src/resources/project/router.spec.ts | 440 +++++++++++++++ .../src/resources/project/router.ts | 199 +++++++ .../old-server/src/resources/queries-index.ts | 14 + .../src/resources/repository/business.ts | 115 ++++ .../src/resources/repository/queries.ts | 59 ++ .../src/resources/repository/router.spec.ts | 402 ++++++++++++++ .../src/resources/repository/router.ts | 135 +++++ .../resources/service-chain/business.spec.ts | 171 ++++++ .../src/resources/service-chain/business.ts | 27 + .../src/resources/service-chain/queries.ts | 58 ++ .../resources/service-chain/router.spec.ts | 306 +++++++++++ .../src/resources/service-chain/router.ts | 90 +++ .../src/resources/service-monitor/business.ts | 9 + .../resources/service-monitor/router.spec.ts | 78 +++ .../src/resources/service-monitor/router.ts | 43 ++ .../src/resources/stage/business.spec.ts | 113 ++++ .../src/resources/stage/business.ts | 97 ++++ .../old-server/src/resources/stage/queries.ts | 111 ++++ .../src/resources/stage/router.spec.ts | 202 +++++++ .../old-server/src/resources/stage/router.ts | 88 +++ .../resources/system/config/business.spec.ts | 22 + .../src/resources/system/config/business.ts | 50 ++ .../src/resources/system/config/queries.ts | 28 + .../resources/system/config/router.spec.ts | 96 ++++ .../src/resources/system/config/router.ts | 36 ++ .../old-server/src/resources/system/index.ts | 1 + .../src/resources/system/router.spec.ts | 25 + .../old-server/src/resources/system/router.ts | 21 + .../src/resources/system/settings/business.ts | 9 + .../src/resources/system/settings/queries.ts | 18 + .../resources/system/settings/router.spec.ts | 67 +++ .../src/resources/system/settings/router.ts | 30 + .../src/resources/user/business.spec.ts | 222 ++++++++ .../old-server/src/resources/user/business.ts | 201 +++++++ .../old-server/src/resources/user/queries.ts | 60 ++ .../src/resources/user/router.spec.ts | 139 +++++ .../old-server/src/resources/user/router.ts | 63 +++ .../src/resources/user/tokens/business.ts | 51 ++ .../src/resources/user/tokens/router.ts | 48 ++ .../src/resources/zone/business.spec.ts | 133 +++++ .../old-server/src/resources/zone/business.ts | 78 +++ .../old-server/src/resources/zone/queries.ts | 21 + .../src/resources/zone/router.spec.ts | 162 ++++++ .../old-server/src/resources/zone/router.ts | 64 +++ .../cpin-module/old-server/src/server.spec.ts | 57 ++ .../src/cpin-module/old-server/src/server.ts | 44 ++ .../old-server/src/utils/business.ts | 41 ++ .../old-server/src/utils/controller.ts | 169 ++++++ .../old-server/src/utils/date.spec.ts | 15 + .../cpin-module/old-server/src/utils/date.ts | 5 + .../cpin-module/old-server/src/utils/env.ts | 57 ++ .../old-server/src/utils/errors.ts | 48 ++ .../old-server/src/utils/fastify.ts | 55 ++ .../old-server/src/utils/hook-wrapper.spec.ts | 235 ++++++++ .../old-server/src/utils/hook-wrapper.ts | 231 ++++++++ .../src/utils/keycloak-utils.spec.ts | 45 ++ .../old-server/src/utils/keycloak-utils.ts | 27 + .../old-server/src/utils/keycloak.ts | 42 ++ .../old-server/src/utils/logger.ts | 97 ++++ .../cpin-module/old-server/src/utils/mocks.ts | 152 ++++++ .../old-server/src/utils/plugins.ts | 9 + .../old-server/src/utils/proxy.spec.ts | 157 ++++++ .../cpin-module/old-server/src/utils/proxy.ts | 78 +++ .../src/utils/queries-tools.spec.ts | 47 ++ .../old-server/src/utils/queries-tools.ts | 11 + .../old-server/src/utils/random.spec.ts | 148 +++++ .../src/cpin-module/old-server/tsconfig.json | 26 + .../src/cpin-module/old-server/vite.config.ts | 18 + .../src/cpin-module/old-server/vitest-init.ts | 11 + .../cpin-module/old-server/vitest.config.ts | 34 ++ apps/server-nestjs/tsconfig.json | 31 +- pnpm-lock.yaml | 515 +++++++++++++++++- 202 files changed, 15541 insertions(+), 26 deletions(-) create mode 100644 apps/server-nestjs/src/cpin-module/cpin.module.ts create mode 100644 apps/server-nestjs/src/cpin-module/cpin/cpin.controller.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/cpin/cpin.controller.ts create mode 100644 apps/server-nestjs/src/cpin-module/cpin/cpin.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/.env-example create mode 100644 apps/server-nestjs/src/cpin-module/old-server/.env.docker-example create mode 100644 apps/server-nestjs/src/cpin-module/old-server/.env.integ-example create mode 100644 apps/server-nestjs/src/cpin-module/old-server/Dockerfile create mode 100644 apps/server-nestjs/src/cpin-module/old-server/README.md create mode 100644 apps/server-nestjs/src/cpin-module/old-server/eslint.config.js create mode 100755 apps/server-nestjs/src/cpin-module/old-server/migrate-db.sh create mode 100644 apps/server-nestjs/src/cpin-module/old-server/nodemon.json create mode 100644 apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/app.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/connect.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230706084346_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230710181052_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230711132934_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230802143822_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230912084459_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231010111515_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125838_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125839_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125841_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231012105520_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231024155020_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231026150220_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240112135751_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240321123436_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240329172938_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240424093852_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240427181037_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240605135052_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240612123132_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240614222908_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240618112205_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240717084709_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240723135420_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240725162050_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240726210139_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240808082632_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240826143230_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240829085548_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240916141253_token/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240919122331_optional_user_id/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923142722_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923155416_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240928002900_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241008125724_enabling_maven/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232540_add_usertype/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232541_add_pat/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241107142721_user_last_login/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112101945_add_slug/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241216131342_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250107104749_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222954_drop_organization/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250723141246_dso/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250818095032_remove_quota/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250916134454_add_project_resources/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/migration_lock.toml create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/admin.prisma create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/schema.prisma create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/token.prisma create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/topography.prisma create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/user.prisma create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/server.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/tsconfig.json create mode 100644 apps/server-nestjs/src/cpin-module/old-server/vite.config.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts create mode 100644 apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts diff --git a/apps/server-nestjs/.prettierrc b/apps/server-nestjs/.prettierrc index a20502b7f..8deec5d94 100644 --- a/apps/server-nestjs/.prettierrc +++ b/apps/server-nestjs/.prettierrc @@ -1,4 +1,12 @@ { - "singleQuote": true, - "trailingComma": "all" + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "importOrderParserPlugins": ["typescript", "decorators"], + "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "printWidth": 80, + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "all" } diff --git a/apps/server-nestjs/package.json b/apps/server-nestjs/package.json index 49db86c0f..37c00e837 100644 --- a/apps/server-nestjs/package.json +++ b/apps/server-nestjs/package.json @@ -20,36 +20,84 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@cpn-console/argocd-plugin": "workspace:^", + "@cpn-console/gitlab-plugin": "workspace:^", + "@cpn-console/harbor-plugin": "workspace:^", + "@cpn-console/hooks": "workspace:^", + "@cpn-console/keycloak-plugin": "workspace:^", + "@cpn-console/kubernetes-plugin": "workspace:^", + "@cpn-console/nexus-plugin": "workspace:^", + "@cpn-console/shared": "workspace:^", + "@cpn-console/sonarqube-plugin": "workspace:^", + "@cpn-console/vault-plugin": "workspace:^", + "@fastify/cookie": "^9.4.0", + "@fastify/helmet": "^11.1.1", + "@fastify/session": "^10.9.0", + "@fastify/swagger": "^8.15.0", + "@fastify/swagger-ui": "^4.2.0", + "@gitbeaker/core": "^40.6.0", + "@gitbeaker/rest": "^40.6.0", + "@kubernetes-models/argo-cd": "^2.6.2", + "@kubernetes/client-node": "^0.22.3", "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "@prisma/client": "^6.0.1", + "@ts-rest/core": "^3.52.1", + "@ts-rest/fastify": "^3.52.1", + "@ts-rest/open-api": "^3.52.1", + "axios": "1.12.2", + "date-fns": "^4.1.0", + "dotenv": "^16.4.7", + "fastify": "^4.29.1", + "fastify-keycloak-adapter": "2.3.2", + "json-2-csv": "^5.5.7", + "mustache": "^4.2.0", + "prisma": "^6.0.1", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "undici": "^7.1.0", + "vitest-mock-extended": "^2.0.2" }, "devDependencies": { + "@cpn-console/eslint-config": "workspace:^", + "@cpn-console/test-utils": "workspace:^", + "@cpn-console/ts-config": "workspace:^", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", + "@faker-js/faker": "^9.3.0", "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", + "@trivago/prettier-plugin-sort-imports": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", + "@vitest/coverage-v8": "^2.1.8", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2", + "fastify-plugin": "^5.0.1", "globals": "^16.0.0", "jest": "^30.0.0", + "nodemon": "^3.1.7", + "pino-pretty": "^13.0.0", "prettier": "^3.4.2", + "rimraf": "^6.0.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", "ts-loader": "^9.5.2", "ts-node": "^10.9.2", + "ts-patch": "^3.3.0", "tsconfig-paths": "^4.2.0", "typescript": "^5.7.3", - "typescript-eslint": "^8.20.0" + "typescript-eslint": "^8.20.0", + "typescript-transform-paths": "^3.5.2", + "vite": "^7.2.1", + "vite-node": "^2.1.8", + "vitest": "^2.1.8" }, "jest": { "moduleFileExtensions": [ diff --git a/apps/server-nestjs/src/app.module.ts b/apps/server-nestjs/src/app.module.ts index 86628031c..465945f4e 100644 --- a/apps/server-nestjs/src/app.module.ts +++ b/apps/server-nestjs/src/app.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { CpinModule } from './cpin-module/cpin.module'; @Module({ - imports: [], + imports: [CpinModule], controllers: [AppController], providers: [AppService], }) diff --git a/apps/server-nestjs/src/app.service.ts b/apps/server-nestjs/src/app.service.ts index 927d7cca0..7263d33a2 100644 --- a/apps/server-nestjs/src/app.service.ts +++ b/apps/server-nestjs/src/app.service.ts @@ -1,8 +1,4 @@ import { Injectable } from '@nestjs/common'; @Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} +export class AppService {} diff --git a/apps/server-nestjs/src/cpin-module/cpin.module.ts b/apps/server-nestjs/src/cpin-module/cpin.module.ts new file mode 100644 index 000000000..935f688fe --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/cpin.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { CpinController } from './cpin/cpin.controller'; +import { CpinService } from './cpin/cpin.service'; + +@Module({ + controllers: [CpinController], + providers: [CpinService] +}) +export class CpinModule {} diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.spec.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.spec.ts new file mode 100644 index 000000000..a7d9aa118 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CpinController } from './cpin.controller'; + +describe('CpinController', () => { + let controller: CpinController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CpinController], + }).compile(); + + controller = module.get(CpinController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.ts new file mode 100644 index 000000000..9ac472902 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('cpin') +export class CpinController {} diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.service.spec.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.service.spec.ts new file mode 100644 index 000000000..0386560c0 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/cpin/cpin.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CpinService } from './cpin.service'; + +describe('CpinService', () => { + let service: CpinService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CpinService], + }).compile(); + + service = module.get(CpinService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts new file mode 100644 index 000000000..f8e75e7df --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts @@ -0,0 +1,143 @@ +import { apiPrefix, getContract } from '@cpn-console/shared'; +import fastifyCookie from '@fastify/cookie'; +import helmet from '@fastify/helmet'; +import fastifySession from '@fastify/session'; +import fastifySwagger from '@fastify/swagger'; +import fastifySwaggerUi from '@fastify/swagger-ui'; +import { Injectable } from '@nestjs/common'; +import { logger } from '@old-server/app'; +import { closeConnections } from '@old-server/connect'; +import { getPreparedApp } from '@old-server/prepare-app'; +import { apiRouter } from '@old-server/resources/index.js'; +import { + isCI, + isDev, + isDevSetup, + isInt, + isProd, + isTest, + port, +} from '@old-server/utils/env.js'; +import { + fastifyConf, + swaggerConf, + swaggerUiConf, +} from '@old-server/utils/fastify.js'; +import { keycloakConf, sessionConf } from '@old-server/utils/keycloak.js'; +import type { CustomLogger } from '@old-server/utils/logger.js'; +import { log } from '@old-server/utils/logger.js'; +import { initServer } from '@ts-rest/fastify'; +import { generateOpenApi } from '@ts-rest/open-api'; +import type { FastifyRequest } from 'fastify'; +import fastify from 'fastify'; +import keycloak from 'fastify-keycloak-adapter'; + +@Injectable() +export class CpinService { + app: any; + + handleExit() { + process.on('exit', this.logExitCode); + process.on('SIGINT', this.exitGracefully); + process.on('SIGTERM', this.exitGracefully); + process.on('uncaughtException', this.exitGracefully); + process.on('unhandledRejection', this.logUnhandledRejection); + } + + logExitCode(code: number) { + logger.warn(`received signal: ${code}`); + } + + logUnhandledRejection(reason: unknown, promise: Promise) { + logger.error({ message: 'Unhandled Rejection', promise, reason }); + } + + async exitGracefully(error?: Error) { + if (error instanceof Error) { + logger.fatal(error); + } + await this.app.close(); + logger.info('Closing connections...'); + await closeConnections(); + logger.info('Exiting...'); + process.exit(error instanceof Error ? 1 : 0); + } + + async getApp(): Promise { + const app = await getPreparedApp(); + + try { + await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }); + } catch (error) { + logger.error(error); + process.exit(1); + } + + logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }); + this.handleExit(); + } + + async createApp() { + const serverInstance: ReturnType = initServer(); + + const openApiDocument = generateOpenApi( + await getContract(), + swaggerConf, + { + setOperationId: true, + }, + ); + + const app = fastify(fastifyConf) + .register(helmet, () => ({ + contentSecurityPolicy: !(isInt || isDev || isTest), + })) + .register(fastifyCookie) + .register(fastifySession, sessionConf) + // @ts-ignore + .register(keycloak, keycloakConf) + .register(fastifySwagger, { + transformObject: () => openApiDocument, + }) + .register(fastifySwaggerUi, swaggerUiConf) + .register(apiRouter()) + .addHook('onRoute', (opts) => { + if (opts.path === `${apiPrefix}/healthz`) { + opts.logLevel = 'silent'; + } + }) + .setErrorHandler((error: Error, req: FastifyRequest, reply) => { + const statusCode = 500; + // @ts-ignore vérifier l'objet + const message = error.description || error.message; + reply.status(statusCode).send({ + status: statusCode, + error: message, + stack: error.stack, + }); + log('info', { reqId: req.id, error }); + }) + .addHook('onResponse', (req, res) => { + if (res.statusCode < 400) { + req.log.info({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } else if (res.statusCode < 500) { + req.log.warn({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } else { + req.log.error({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } + }); + + await app.ready(); + + const logger = app.log as CustomLogger; + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/.env-example b/apps/server-nestjs/src/cpin-module/old-server/.env-example new file mode 100644 index 000000000..84236efbe --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/.env-example @@ -0,0 +1,18 @@ +DEV_SETUP="true" +NODE_ENV=development +# HOME=/home/node +SESSION_SECRET=a-very-strong-secret-with-more-than-32-char +KEYCLOAK_DOMAIN=localhost:8090 +KEYCLOAK_REALM=cloud-pi-native +KEYCLOAK_PROTOCOL=http +KEYCLOAK_CLIENT_ID=dso-console-backend +KEYCLOAK_CLIENT_SECRET=client-secret-backend +KEYCLOAK_REDIRECT_URI=http://localhost:8080 +SERVER_PORT=4000 +DB_URL=postgresql://admin:admin@localhost:5432/dso-console-db?schema=public +CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr + +# Configuration OpenCDS +OPENCDS_URL= +OPENCDS_API_TOKEN=token +OPENCDS_API_TLS_REJECT_UNAUTHORIZED=true diff --git a/apps/server-nestjs/src/cpin-module/old-server/.env.docker-example b/apps/server-nestjs/src/cpin-module/old-server/.env.docker-example new file mode 100644 index 000000000..0da54a8e4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/.env.docker-example @@ -0,0 +1,13 @@ +DOCKER=true +DEV_SETUP="true" +NODE_ENV=development +SESSION_SECRET=a-very-strong-secret-with-more-than-32-char +KEYCLOAK_DOMAIN=keycloak:8080 +KEYCLOAK_REALM=cloud-pi-native +KEYCLOAK_PROTOCOL=http +KEYCLOAK_CLIENT_ID=dso-console-backend +KEYCLOAK_CLIENT_SECRET=client-secret-backend +KEYCLOAK_REDIRECT_URI=http://localhost:8080 +SERVER_PORT=8080 +DB_URL=postgresql://admin:admin@postgres:5432/dso-console-db?schema=public +CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr diff --git a/apps/server-nestjs/src/cpin-module/old-server/.env.integ-example b/apps/server-nestjs/src/cpin-module/old-server/.env.integ-example new file mode 100644 index 000000000..33e23e778 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/.env.integ-example @@ -0,0 +1,43 @@ +DEV_SETUP="false" +INTEGRATION=true +KEYCLOAK_PROTOCOL=https +KEYCLOAK_CLIENT_ID= +KEYCLOAK_CLIENT_SECRET= +KEYCLOAK_DOMAIN= +KEYCLOAK_REALM= +ARGO_NAMESPACE= +ARGOCD_URL= +GITLAB_TOKEN= +GITLAB_URL= +HARBOR_ADMIN= +HARBOR_ADMIN_PASSWORD= +HARBOR_URL= +KEYCLOAK_ADMIN= +KEYCLOAK_ADMIN_PASSWORD= +KEYCLOAK_URL= +NEXUS_ADMIN= +NEXUS_ADMIN_PASSWORD= +NEXUS_URL= +PROJECTS_ROOT_DIR= +SONAR_API_TOKEN= +SONARQUBE_URL= +VAULT_TOKEN= +VAULT_URL= + +KUBECONFIG_HOST_PATH= +KUBECONFIG_PATH=$HOME/.kube/config +KUBECONFIG_CTX= + +EXTERNAL_PLUGINS_DIR_HOST_PATH=/path/to/plugins + +# Variables de plugins externes + +# GRAVITEE_APIM_API_ID= +# GRAVITEE_APIM_PLAN_ID= +# GRAVITEE_APIM_TOKEN= +# GRAVITEE_APIM_URL= +# GRAVITEE_GATEWAY_URL= + +# HTTP_PROXY= +# HTTPS_PROXY= +# NO_PROXY= diff --git a/apps/server-nestjs/src/cpin-module/old-server/Dockerfile b/apps/server-nestjs/src/cpin-module/old-server/Dockerfile new file mode 100644 index 000000000..71c0b3c02 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/Dockerfile @@ -0,0 +1,66 @@ +# Base stage ----------------------------------------------------------------------- +FROM docker.io/node:22.14.0-bullseye-slim AS dev + +WORKDIR /app + +COPY --chown=node:root package.json ./ + +# Install pnpm version defined in package.json "packageManager" property +RUN npm install --global corepack@latest && corepack enable && corepack enable pnpm + +COPY --chown=node:root pnpm-workspace.yaml pnpm-lock.yaml .npmrc turbo.json ./ +COPY --chown=node:root patches ./patches +COPY --chown=node:root apps/server/package.json ./apps/server/package.json + +COPY --chown=node:root packages/eslintconfig/package.json ./packages/eslintconfig/package.json +COPY --chown=node:root packages/shared/package.json ./packages/shared/package.json +COPY --chown=node:root packages/hooks/package.json ./packages/hooks/package.json +COPY --chown=node:root packages/test-utils/package.json ./packages/test-utils/package.json +COPY --chown=node:root packages/tsconfig/package.json ./packages/tsconfig/package.json + +COPY --chown=node:root plugins/argocd/package.json ./plugins/argocd/package.json +COPY --chown=node:root plugins/gitlab/package.json ./plugins/gitlab/package.json +COPY --chown=node:root plugins/harbor/package.json ./plugins/harbor/package.json +COPY --chown=node:root plugins/keycloak/package.json ./plugins/keycloak/package.json +COPY --chown=node:root plugins/kubernetes/package.json ./plugins/kubernetes/package.json +COPY --chown=node:root plugins/nexus/package.json ./plugins/nexus/package.json +COPY --chown=node:root plugins/sonarqube/package.json ./plugins/sonarqube/package.json +COPY --chown=node:root plugins/vault/package.json ./plugins/vault/package.json + +RUN pnpm install --frozen-lockfile +COPY --chown=node:root plugins/ ./plugins/ +COPY --chown=node:root packages/ ./packages/ + +COPY --chown=node:root apps/server/ ./apps/server/ +# Generate Prisma client +RUN pnpm --filter server run db:generate +ENTRYPOINT [ "pnpm", "--filter", "server", "run" ] +CMD [ "dev" ] + + +# Build stage ---------------------------------------------------------------------- +FROM dev AS build + +# Build @cpn-console/server console-related dependencies +RUN pnpm run build +# Build @cpn-console/server +RUN pnpm --filter @cpn-console/server run build +# Export @cpn-console/server to target build directory +RUN pnpm --filter @cpn-console/server --prod deploy build + + +# Prod stage ----------------------------------------------------------------------- +FROM docker.io/node:22.14.0-bullseye-slim AS prod + +ARG APP_VERSION +ENV APP_VERSION=$APP_VERSION +VOLUME [ "/plugins" ] +WORKDIR /app +RUN mkdir -p /home/node/logs && chmod 770 -R /home/node/logs \ + && mkdir -p /home/node/.npm && chmod 770 -R /home/node/.npm \ + && chown node:root /app +COPY --chown=node:root --from=build /app/build . +RUN npm run db:generate +USER node +EXPOSE 8080 +ENTRYPOINT ["npm", "start"] diff --git a/apps/server-nestjs/src/cpin-module/old-server/README.md b/apps/server-nestjs/src/cpin-module/old-server/README.md new file mode 100644 index 000000000..37b89ac99 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/README.md @@ -0,0 +1,39 @@ +# Console Cloud π Native - Serveur + +## Installation + +```sh +npm install +``` + +## Lancement de l'app + +```sh +npm run dev +``` + +## Lancement des tests unitaires + +```sh +npm run test +``` + +## Formattage du code + +```sh +# Lister les problèmes de formatage +npm run lint + +# Régler automatiquement les problèmes de formatage +npm run format +``` + +## Lancement de l'app en mode production + +```sh +npm start +``` + +## Activation OpenCDS + +Se référer à [la documentation dédiée](../../packages/opencds/README.adoc) diff --git a/apps/server-nestjs/src/cpin-module/old-server/eslint.config.js b/apps/server-nestjs/src/cpin-module/old-server/eslint.config.js new file mode 100644 index 000000000..5a664d2b5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/eslint.config.js @@ -0,0 +1,3 @@ +import eslintConfigBase from '@cpn-console/eslint-config' + +export default eslintConfigBase diff --git a/apps/server-nestjs/src/cpin-module/old-server/migrate-db.sh b/apps/server-nestjs/src/cpin-module/old-server/migrate-db.sh new file mode 100755 index 000000000..a65db96cb --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/migrate-db.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +set -e + +# Colorize terminal +red='\e[0;31m' +no_color='\033[0m' + +# Default +RESET_DB="false" + +# DB Values +DB_NAME=dso-console-db +DB_PORT=5432 +DB_USER=admin +DB_PASS=admin + +# Declare script helper +TEXT_HELPER="\nThis script aims to perform prisma migrations. + +It is needed to export shell variables 'DB_USER', 'DB_PASS', 'DB_PORT' and 'DB_NAME'. Default are : + + DB_USER: $DB_USER + DB_PASS: $DB_PASS + DB_NAME: $DB_NAME + +Following flags are available: + + -r Reset the database. Default is "$RESET_DB". + + -h Print script help.\n\n" + +print_help() { + printf "$TEXT_HELPER" +} + +# Parse options +while getopts hr flag +do + case "${flag}" in + r) + RESET_DB=true;; + h | *) + print_help + exit 0;; + esac +done + + +# Override database variables for local access +export DB_URL="postgresql://$DB_USER:$DB_PASS@localhost:$DB_PORT/$DB_NAME?schema=public" + +# Start database container +printf "\n${red}[db wrapper]${no_color}: Start postgres container\n" +docker run \ + --name postgres-migration \ + --publish $DB_PORT:$DB_PORT \ + --env POSTGRES_USER=$DB_USER \ + --env POSTGRES_PASSWORD=$DB_PASS \ + --env POSTGRES_DB=$DB_NAME \ + --detach \ + postgres + +sleep 3 + +# Start prisma migration +if [ "$RESET_DB" = "true" ]; then + printf "\n${red}[db wrapper]${no_color}: Start prisma reset\n" + pnpm --filter @cpn-console/server run db:reset +fi +printf "\n${red}[db wrapper]${no_color}: Start prisma migration\n" +pnpm --filter @cpn-console/server run db:migrate + +# Stop database container +printf "\n${red}[db wrapper]${no_color}: Stop and remove postgres container\n" +docker stop postgres-migration +docker rm postgres-migration diff --git a/apps/server-nestjs/src/cpin-module/old-server/nodemon.json b/apps/server-nestjs/src/cpin-module/old-server/nodemon.json new file mode 100644 index 000000000..a83d0540d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/nodemon.json @@ -0,0 +1,4 @@ +{ + "watch": ["server.ts", "src/"], + "ext": "js, ts" +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts b/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts new file mode 100644 index 000000000..057121c97 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts @@ -0,0 +1,9 @@ +import path from 'node:path' +import { defineConfig } from 'prisma/config' + +export default defineConfig({ + schema: path.join('src', 'prisma', 'schema'), + migrations: { + path: path.join('src', 'prisma', 'migrations'), + }, +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts new file mode 100644 index 000000000..075578c96 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts @@ -0,0 +1,14 @@ +import type { PrismaClient } from '@prisma/client' +import { beforeEach, vi } from 'vitest' +import { mockDeep, mockReset } from 'vitest-mock-extended' + +vi.mock('../prisma.js') + +const prisma = mockDeep() + +beforeEach(() => { + // reset les mocks + mockReset(prisma) +}) + +export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts new file mode 100644 index 000000000..34c7bee26 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts @@ -0,0 +1,34 @@ +import { beforeEach, vi } from 'vitest' +import { mockDeep, mockReset } from 'vitest-mock-extended' + +vi.mock('../utils/hook-wrapper.ts') + +export const hook = { + cluster: { + delete: vi.fn(), + upsert: vi.fn(), + }, + misc: { + checkServices: vi.fn(), + syncRepository: vi.fn(), + }, + project: { + upsert: vi.fn(), + delete: vi.fn(), + getSecrets: vi.fn(), + }, + user: { + retrieveUserByEmail: vi.fn(), + }, + zone: { + delete: vi.fn(), + upsert: vi.fn(), + }, +} as const + +const hookMock = mockDeep() + +beforeEach(() => { + // reset les mocks + mockReset(hookMock) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts new file mode 100644 index 000000000..a09ea94b5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { apiPrefix } from '@cpn-console/shared' +import app from './app.js' +import { getRandomRequestor, setRequestor } from './utils/mocks.js' + +vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) + +describe('app', () => { + beforeEach(() => { + setRequestor(getRandomRequestor()) + }) + afterAll(async () => { + await app.close() + }) + + it('should respond 404 on unknown route', async () => { + const response = await app.inject() + .get(`${apiPrefix}/miss`) + expect(response.statusCode).toBe(404) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts new file mode 100644 index 000000000..c525cf161 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts @@ -0,0 +1,59 @@ +import type { FastifyRequest } from 'fastify' +import fastify from 'fastify' +import helmet from '@fastify/helmet' +import keycloak from 'fastify-keycloak-adapter' +import fastifySession from '@fastify/session' +import fastifyCookie from '@fastify/cookie' +import fastifySwagger from '@fastify/swagger' +import fastifySwaggerUi from '@fastify/swagger-ui' +import { initServer } from '@ts-rest/fastify' +import { generateOpenApi } from '@ts-rest/open-api' +import { apiPrefix, getContract } from '@cpn-console/shared' +import { isDev, isInt, isTest } from './utils/env.js' +import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify.js' +import { apiRouter } from './resources/index.js' +import { keycloakConf, sessionConf } from './utils/keycloak.js' +import type { CustomLogger } from './utils/logger.js' +import { log } from './utils/logger.js' + +export const serverInstance: ReturnType = initServer() + +const openApiDocument = generateOpenApi(await getContract(), swaggerConf, { setOperationId: true }) + +const app = fastify(fastifyConf) + .register(helmet, () => ({ + contentSecurityPolicy: !(isInt || isDev || isTest), + })) + .register(fastifyCookie) + .register(fastifySession, sessionConf) + // @ts-ignore + .register(keycloak, keycloakConf) + .register(fastifySwagger, { transformObject: () => openApiDocument }) + .register(fastifySwaggerUi, swaggerUiConf) + .register(apiRouter()) + .addHook('onRoute', (opts) => { + if (opts.path === `${apiPrefix}/healthz`) { + opts.logLevel = 'silent' + } + }) + .setErrorHandler((error: Error, req: FastifyRequest, reply) => { + const statusCode = 500 + // @ts-ignore vérifier l'objet + const message = error.description || error.message + reply.status(statusCode).send({ status: statusCode, error: message, stack: error.stack }) + log('info', { reqId: req.id, error }) + }) + .addHook('onResponse', (req, res) => { + if (res.statusCode < 400) { + req.log.info({ status: res.statusCode, userId: req.session?.user?.id }) + } else if (res.statusCode < 500) { + req.log.warn({ status: res.statusCode, userId: req.session?.user?.id }) + } else { + req.log.error({ status: res.statusCode, userId: req.session?.user?.id }) + } + }) + +await app.ready() + +export const logger = app.log as CustomLogger +export default app diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts new file mode 100644 index 000000000..2c1a40cee --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts @@ -0,0 +1,61 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PrismaClientInitializationError } from '@prisma/client/runtime/library.js' +import prisma from './__mocks__/prisma.js' +import app, { logger } from './app.js' +import { getConnection } from './connect.js' + +vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) +vi.mock('@/resources/queries-index.js') +vi.mock('./models/log.js', () => getModel('getLogModel')) +vi.mock('./models/repository.js', () => getModel('getRepositoryModel')) +vi.mock('./models/permission.js', () => getModel('getPermissionModel')) +vi.mock('./models/environment.js', () => getModel('getEnvironmentModel')) +vi.mock('./models/project.js', () => getModel('getProjectModel')) +vi.mock('./models/user.js', () => getModel('getUserModel')) +vi.mock('./models/users-projects.js', () => getModel('getRolesModel')) +vi.mock('./models/zone.js', () => getModel('getZoneModel')) +vi.mock('./prisma.js') + +vi.spyOn(app, 'listen') +vi.spyOn(logger, 'info') +vi.spyOn(logger, 'warn') +vi.spyOn(logger, 'error') +vi.spyOn(logger, 'debug') + +function getModel(modelName) { + return { + [modelName]: vi.fn(() => ({ + sync: vi.fn(), + hasMany: vi.fn(), + belongsTo: vi.fn(), + belongsToMany: vi.fn(), + })), + } +} + +describe('connect', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should connect to postgres', async () => { + await getConnection() + + expect(logger.info.mock.calls).toHaveLength(2) + expect(logger.info.mock.calls).toContainEqual([`Trying to connect to Postgres with: ${process.env.DB_URL}`]) + expect(logger.info.mock.calls).toContainEqual(['Connected to Postgres!']) + }) + + it('should fail to connect once, then connect to postgres', async () => { + const errorToCatch = new PrismaClientInitializationError('Failed to connect', '2.19.0', 'P1001') + + prisma.$connect.mockRejectedValueOnce(errorToCatch) + await getConnection() + + expect(logger.info.mock.calls).toHaveLength(5) + expect(logger.info.mock.calls).toContainEqual([`Trying to connect to Postgres with: ${process.env.DB_URL}`]) + expect(logger.info.mock.calls).toContainEqual(['Could not connect to Postgres: Failed to connect']) + expect(logger.info.mock.calls).toContainEqual(['Retrying (4 tries left)']) + expect(logger.info.mock.calls).toContainEqual(['Connected to Postgres!']) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts new file mode 100644 index 000000000..ae15b49d8 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts @@ -0,0 +1,52 @@ +import { setTimeout } from 'node:timers/promises' +import prisma from './prisma.js' +import { logger } from './app.js' +import { + dbUrl, + isCI, + isDev, + isTest, +} from './utils/env.js' + +const DELAY_BEFORE_RETRY = isTest || isCI ? 1000 : 10000 +let closingConnections = false + +export async function getConnection(triesLeft = 5): Promise { + if (closingConnections || triesLeft <= 0) { + throw new Error('Unable to connect to Postgres server') + } + triesLeft-- + + try { + if (isDev || isTest || isCI) { + logger.info(`Trying to connect to Postgres with: ${dbUrl}`) + } + await prisma.$connect() + + logger.info('Connected to Postgres!') + } catch (error) { + if (triesLeft > 0) { + logger.error(error) + logger.info(`Could not connect to Postgres: ${error.message}`) + logger.info(`Retrying (${triesLeft} tries left)`) + await setTimeout(DELAY_BEFORE_RETRY) + return getConnection(triesLeft) + } + + logger.info(`Could not connect to Postgres: ${error.message}`) + logger.info('Out of retries') + error.message = `Out of retries, last error: ${error.message}` + throw error + } +} + +export async function closeConnections() { + closingConnections = true + try { + await prisma.$disconnect() + } catch (error) { + logger.error(error) + } finally { + closingConnections = false + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts new file mode 100644 index 000000000..42e6c75cb --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts @@ -0,0 +1,28 @@ +// @ts-nocheck + +/** + * How to use ? + * npx vite-node src/init/db/dump.ts + * format ./data.ts with linter + * cut/paste to packages/test-utils/src/imports/data.ts + */ + +import { writeFileSync } from 'node:fs' +import { Prisma } from '@prisma/client' +import { associations, manyToManyRelation, modelKeys, models, resourceListToDict } from './utils.js' +import prisma from '@/prisma.js' + +const Models = resourceListToDict(Prisma.dmmf.datamodel.models) + +for (const modelKey of modelKeys) { + const modelDatas = await prisma[modelKey].findMany() + models[modelKey] = modelDatas +} +for (const [model, targetModel, relationKey] of manyToManyRelation) { + const modelKey = model.slice(0, 1).toLocaleLowerCase() + model.slice(1) + const modelDatas = await prisma[modelKey].findMany({ select: { [Models[model].id]: true, [relationKey]: { select: { [Models[targetModel].id]: true } } } }) + associations.push([modelKey, modelDatas]) +} +const a = JSON.stringify({ ...models, associations }, null, 2) + +writeFileSync('./data.ts', `export const data = ${a}`) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts new file mode 100644 index 000000000..a2788130e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts @@ -0,0 +1,51 @@ +import { modelKeys } from './utils.js' +import { logger } from '@/app.js' +import prisma from '@/prisma.js' + +type ExtractKeysWithFields = { + [K in keyof T]: T[K] extends { fields: any } ? K : never +}[keyof T] + +type Models = ExtractKeysWithFields + +type Imports = Partial> & { + associations: [Models, any[]] +} + +export async function initDb(data: Imports) { + const dataStringified = JSON.stringify(data) + const dataParsed = JSON.parse(dataStringified, (key, value) => { + try { + if (['permissions', 'everyonePerms'].includes(key)) { + return BigInt(value.slice(0, value.length - 1)) + } + } catch (_error) { + return value + } + return value + }) + logger.info('Drop tables') + for (const modelKey of modelKeys.toReversed()) { + // @ts-ignore + await prisma[modelKey].deleteMany() + } + logger.info('Import models') + for (const modelKey of modelKeys) { + // @ts-ignore + await prisma[modelKey].createMany({ data: dataParsed[modelKey] }) + } + logger.info('Import associations') + for (const [modelKey, rows] of dataParsed.associations) { + for (const row of rows) { + const idKey = 'id' + const connectKeys = Object.keys(row).filter(key => key !== idKey) + const dataConnects = connectKeys.reduce((acc, curr) => { + acc[curr] = { connect: row[curr] } + return acc + }, {} as Record) + // @ts-ignore + await prisma[modelKey].update({ where: { id: row.id }, data: dataConnects }) + } + } + logger.info('End import') +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts new file mode 100644 index 000000000..9b831ee08 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts @@ -0,0 +1,52 @@ +import { describe, expect, it, vi } from 'vitest' +import prisma from '../../__mocks__/prisma.js' +import { modelKeys, moveBefore, resourceListToDict } from './utils.js' + +vi.mock('fs', () => ({ writeFileSync: vi.fn() })) +for (const modelKey of modelKeys) { + prisma[modelKey].findMany.mockResolvedValue([]) +} + +describe('test moveBefore', () => { + it('should be moved', () => { + const arr = ['a', 'b', 'c'] + const arrSorted = moveBefore(arr, 'c', 'b') + expect(arrSorted).toEqual(['a', 'c', 'b']) + + const arrSorted2 = moveBefore(arr, 'c', 'a') + expect(arrSorted2).toEqual(['c', 'a', 'b']) + }) + it('should not be moved', () => { + const arr = ['a', 'b', 'c'] + const arrSorted = moveBefore(arr, 'b', 'c') + expect(arrSorted).toEqual(false) + + const arrSorted2 = moveBefore(arr, 'a', 'c') + expect(arrSorted2).toEqual(false) + + const arrSorted3 = moveBefore(arr, 'c', 'c') + expect(arrSorted3).toEqual(false) + }) +}) + +it('test resourceListToDict (by name)', () => { + const list = [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 }, + { name: 'c', value: 3 }, + ] + const dict = resourceListToDict(list) + expect(dict).toEqual({ + a: { name: 'a', value: 1 }, + b: { name: 'b', value: 2 }, + c: { name: 'c', value: 3 }, + }) +}) + +it('stringify bigint', () => { + const list = { name: 'a', value: 1n } + + const dict = JSON.stringify(list) + + expect(dict).toEqual('{"name":"a","value":"1n"}') +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts new file mode 100644 index 000000000..95924751a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts @@ -0,0 +1,85 @@ +// @ts-nocheck +import { Prisma } from '@prisma/client' + +// eslint-disable-next-line no-extend-native +BigInt.prototype.toJSON = function () { + return `${this.toString()}n` +} + +export type ResourceByName = Record +export function resourceListToDict(resList: Array): ResourceByName { + return resList.reduce((acc, curr) => { + return { + ...acc, + [curr.name]: curr, + } + }, {} as ResourceByName) +} + +// @ts-ignore +const Models = resourceListToDict(Prisma.dmmf.datamodel.models) +let ModelsNames = Object.keys(Models) +let ModelsOrder = [...ModelsNames] + +export function moveBefore(arr: T, toMove: T[number], ref: T[number]): T | false { + const iref = arr.indexOf(ref) + const moveref = arr.indexOf(toMove) + if (moveref <= iref) return false + return [ + ...arr.slice(0, iref), + arr[moveref], + ...arr.slice(iref, moveref), + ...arr.slice(moveref + 1), + ] as T +} + +export const manyToManyRelation: [string, string, string][] = [] +function sort() { + let hasChanged = false + for (const model of ModelsNames) { + for (const field of Models[model].fields) { + if (field.isId) Models[model].id = field.name + if (field.type in Models) { + const relationField = Models[field.type].fields.find(({ type }) => type === model) + if (!relationField) throw new Error('unable to find matching model') + if ( + (relationField.isRequired && field.isRequired && !relationField.isList) + || (relationField.isRequired && !field.isRequired) + ) { + const moveRes = moveBefore(ModelsOrder, model, field.type) + if (moveRes) { + hasChanged = true + ModelsOrder = moveRes + } + } + if ( + field.isList && relationField.isList + && !manyToManyRelation.find(test => + (test[0] === model && test[1] === field.type) || (test[0] === field.type && test[1] === model)) + ) { + manyToManyRelation.push([model, field.type, field.name]) + } + } + } + } + ModelsNames = ModelsOrder + if (hasChanged) sort() +} + +sort() + +// special case to study +const logUserCase = moveBefore(ModelsOrder, 'User', 'Log') +if (logUserCase) { + ModelsOrder = logUserCase +} +const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log') +if (logProjectCase) { + ModelsOrder = logProjectCase +} + +export const models: Record = {} +export const associations: Record = [] +export const modelKeys = ModelsOrder.map(model => model.slice(0, 1).toLocaleLowerCase() + model.slice(1)) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts new file mode 100644 index 000000000..075578c96 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts @@ -0,0 +1,14 @@ +import type { PrismaClient } from '@prisma/client' +import { beforeEach, vi } from 'vitest' +import { mockDeep, mockReset } from 'vitest-mock-extended' + +vi.mock('../prisma.js') + +const prisma = mockDeep() + +beforeEach(() => { + // reset les mocks + mockReset(prisma) +}) + +export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts new file mode 100644 index 000000000..3e9556625 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts @@ -0,0 +1,24 @@ +import fp from 'fastify-plugin' +import type { User } from '@cpn-console/test-utils' + +let requestor: User + +export function setRequestor(user: User) { + requestor = user +} + +export function getRequestor() { + return requestor +} + +export async function mockSessionPlugin() { + const sessionPlugin = (app, opt, next) => { + app.addHook('onRequest', (req, res, next) => { + req.session = { user: getRequestor() } + next() + }) + next() + } + + return { default: fp(sessionPlugin) } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts new file mode 100644 index 000000000..9e9f245ce --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts @@ -0,0 +1,46 @@ +import { readdirSync, statSync } from 'node:fs' +import { type Plugin, pluginManager } from '@cpn-console/hooks' +import { plugin as argo } from '@cpn-console/argocd-plugin' +import { plugin as gitlab } from '@cpn-console/gitlab-plugin' +import { plugin as harbor } from '@cpn-console/harbor-plugin' +import { plugin as keycloak } from '@cpn-console/keycloak-plugin' +import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin' +import { plugin as nexus } from '@cpn-console/nexus-plugin' +import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin' +import { plugin as vault } from '@cpn-console/vault-plugin' +import { pluginManagerOptions } from './utils/plugins.js' +import { pluginsDir } from './utils/env.js' + +export async function initPm() { + const pm = pluginManager(pluginManagerOptions) + pm.register(argo) + pm.register(gitlab) + pm.register(harbor) + pm.register(keycloak) + pm.register(kubernetes) + pm.register(nexus) + pm.register(sonarqube) + pm.register(vault) + + if (!statSync(pluginsDir, { + throwIfNoEntry: false, + })) { + return pm + } + for (const dirName of readdirSync(pluginsDir)) { + const moduleAbsPath = `${pluginsDir}/${dirName}` + try { + statSync(`${moduleAbsPath}/package.json`) + const pkg = await import(`${moduleAbsPath}/package.json`, { with: { type: 'json' } }) + const entrypoint = pkg.default.module || pkg.default.main + if (!entrypoint) throw new Error(`No entrypoint found in package.json : ${pkg.default.name}`) + const { plugin } = await import(`${moduleAbsPath}/${entrypoint}`) as { plugin: Plugin } + pm.register(plugin) + } catch (error) { + console.error(`Could not import module ${moduleAbsPath}`) + console.error(error.stack) + } + } + + return pm +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts new file mode 100644 index 000000000..292cbdb29 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getPreparedApp } from './prepare-app.js' +import { getConnection } from './connect.js' +import { initDb } from './init/db/index.js' +import app, { logger } from './app.js' + +vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) +vi.mock('./connect.js') +vi.mock('./index.js') +vi.mock('./utils/logger.js') +vi.mock('./init/db/index.js', () => ({ initDb: vi.fn() })) + +vi.spyOn(app, 'listen') +vi.spyOn(logger, 'info') +vi.spyOn(logger, 'warn') +vi.spyOn(logger, 'error') +vi.spyOn(logger, 'debug') + +describe('server', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should getConnection', async () => { + // const port = Math.round(Math.random() * 10000) + 1024 + await getPreparedApp().catch(err => console.warn(err)) + + expect(getConnection).toHaveBeenCalledTimes(1) + expect(initDb.mock.calls).toHaveLength(1) + }) + + it('should throw an error on connection to DB', async () => { + const error = new Error('This is OK!') + getConnection.mockRejectedValueOnce(error) + + let response + await getPreparedApp() + .catch((err) => { response = err }) + + expect(getConnection.mock.calls).toHaveLength(1) + expect(app.listen.mock.calls).toHaveLength(0) + expect(response).toMatchObject(error) + }) + + it('should throw an error on initDb import if module is not found', async () => { + const error = new Error('Failed to load') + initDb.mockRejectedValueOnce(error) + + await getPreparedApp() + + expect(initDb.mock.calls).toHaveLength(1) + expect(logger.info.mock.calls).toHaveLength(3) + }) + + it('should throw an error on initDb import', async () => { + const error = new Error('This is OK!') + initDb.mockRejectedValueOnce(error) + + let response + try { + await getPreparedApp() + } catch (err) { + response = err + } + + expect(initDb.mock.calls).toHaveLength(1) + expect(logger.info.mock.calls).toHaveLength(2) + expect(response).toMatchObject(error) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts new file mode 100644 index 000000000..142f9b33e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts @@ -0,0 +1,106 @@ +import { rm } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { isCI, isDev, isDevSetup, isInt, isProd, isTest, port } from './utils/env.js' +import app, { logger } from './app.js' +import { getConnection } from './connect.js' +import { initDb } from './init/db/index.js' +import { initPm } from './plugins.js' + +// Workaround because fetch isn't using http_proxy variables +// See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 +if (process.env.HTTP_PROXY) { + const Undici = await import('undici') + const ProxyAgent = Undici.ProxyAgent + const setGlobalDispatcher = Undici.setGlobalDispatcher + setGlobalDispatcher( + new ProxyAgent(process.env.HTTP_PROXY), + ) +} + +async function initializeDB(path: string) { + logger.info('Starting init DB...') + const { data } = await import(path) + await initDb(data) + logger.info('initDb invoked successfully') +} + +export async function startServer(defaultPort: number = (port ? +port : 8080)) { + try { + await getConnection() + } catch (error) { + if (!(error instanceof Error)) return + logger.error(error.message) + throw error + } + + initPm() + + logger.info('Reading init database file') + + try { + const dataPath = (isProd || isInt) + ? './init/db/imports/data.js' + : '@cpn-console/test-utils/src/imports/data.ts' + await initializeDB(dataPath) + if (isProd && !isDevSetup) { + logger.info('Cleaning up imported data file...') + const __filename = fileURLToPath(import.meta.url) + const __dirname = dirname(__filename) + await rm(resolve(__dirname, dataPath)) + logger.info(`Successfully deleted '${dataPath}'`) + } + } catch (error) { + if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { + logger.info('No initDb file, skipping') + } else { + logger.warn(error.message) + throw error + } + } + + try { + await app.listen({ host: '0.0.0.0', port: defaultPort ?? 8080 }) + } catch (error) { + logger.error(error) + process.exit(1) + } + logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) +} + +export async function getPreparedApp() { + try { + await getConnection() + } catch (error) { + logger.error(error.message) + throw error + } + + initPm() + + logger.info('Reading init database file') + + try { + const dataPath = (isProd || isInt) + ? './init/db/imports/data.js' + : '@cpn-console/test-utils/src/imports/data.ts' + await initializeDB(dataPath) + if (isProd && !isDevSetup) { + logger.info('Cleaning up imported data file...') + const __filename = fileURLToPath(import.meta.url) + const __dirname = dirname(__filename) + await rm(resolve(__dirname, dataPath)) + logger.info(`Successfully deleted '${dataPath}'`) + } + } catch (error) { + if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { + logger.info('No initDb file, skipping') + } else { + logger.warn(error.message) + throw error + } + } + + logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) + return app +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts new file mode 100644 index 000000000..4590932b6 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230706084346_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230706084346_dso/migration.sql new file mode 100644 index 000000000..f2f4e7b0b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230706084346_dso/migration.sql @@ -0,0 +1,151 @@ +-- CreateTable +CREATE TABLE "Environment" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "projectId" UUID NOT NULL, + "status" TEXT NOT NULL DEFAULT 'initializing', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Environment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Log" ( + "id" UUID NOT NULL, + "data" JSONB NOT NULL, + "action" TEXT NOT NULL DEFAULT '', + "userId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Log_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Organization" ( + "id" UUID NOT NULL, + "source" TEXT NOT NULL, + "name" TEXT NOT NULL, + "label" TEXT NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Permission" ( + "id" UUID NOT NULL, + "userId" UUID NOT NULL, + "environmentId" UUID NOT NULL, + "level" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Permission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Project" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "organizationId" UUID NOT NULL, + "description" TEXT, + "status" TEXT NOT NULL, + "locked" BOOLEAN NOT NULL DEFAULT false, + "services" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Repository" ( + "id" UUID NOT NULL, + "projectId" UUID NOT NULL, + "internalRepoName" TEXT NOT NULL, + "externalRepoUrl" TEXT NOT NULL, + "externalUserName" TEXT, + "externalToken" TEXT, + "isInfra" BOOLEAN NOT NULL DEFAULT false, + "isPrivate" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'initializing', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Repository_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" UUID NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Role" ( + "userId" UUID NOT NULL, + "projectId" UUID NOT NULL, + "role" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Role_pkey" PRIMARY KEY ("userId","projectId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_id_key" ON "Organization"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_name_key" ON "Organization"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_label_key" ON "Organization"("label"); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_id_key" ON "Permission"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_userId_environmentId_key" ON "Permission"("userId", "environmentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Project_id_key" ON "Project"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Role_userId_projectId_key" ON "Role"("userId", "projectId"); + +-- AddForeignKey +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Log" ADD CONSTRAINT "Log_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Permission" ADD CONSTRAINT "Permission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Permission" ADD CONSTRAINT "Permission_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Repository" ADD CONSTRAINT "Repository_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Role" ADD CONSTRAINT "Role_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230710181052_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230710181052_dso/migration.sql new file mode 100644 index 000000000..26e1ade3f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230710181052_dso/migration.sql @@ -0,0 +1,85 @@ +-- CreateEnum +CREATE TYPE "ClusterPrivacy" AS ENUM ('public', 'dedicated'); + +-- CreateTable +CREATE TABLE "Cluster" ( + "id" UUID NOT NULL, + "label" VARCHAR(50) NOT NULL, + "privacy" "ClusterPrivacy" NOT NULL DEFAULT 'dedicated', + "secretName" VARCHAR(50) NOT NULL, + "clusterResources" BOOLEAN NOT NULL DEFAULT false, + "kubeConfigId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Cluster_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Kubeconfig" ( + "id" UUID NOT NULL, + "user" JSONB NOT NULL, + "cluster" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "parentClusterId" UUID, + + CONSTRAINT "Kubeconfig_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_ClusterToEnvironment" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_ClusterToProject" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Cluster_id_key" ON "Cluster"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Cluster_label_key" ON "Cluster"("label"); + +-- CreateIndex +CREATE UNIQUE INDEX "Cluster_secretName_key" ON "Cluster"("secretName"); + +-- CreateIndex +CREATE UNIQUE INDEX "Cluster_kubeConfigId_key" ON "Cluster"("kubeConfigId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Kubeconfig_id_key" ON "Kubeconfig"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Kubeconfig_parentClusterId_key" ON "Kubeconfig"("parentClusterId"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ClusterToEnvironment_AB_unique" ON "_ClusterToEnvironment"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ClusterToEnvironment_B_index" ON "_ClusterToEnvironment"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ClusterToProject_AB_unique" ON "_ClusterToProject"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ClusterToProject_B_index" ON "_ClusterToProject"("B"); + +-- AddForeignKey +ALTER TABLE "Cluster" ADD CONSTRAINT "Cluster_kubeConfigId_fkey" FOREIGN KEY ("kubeConfigId") REFERENCES "Kubeconfig"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToEnvironment" ADD CONSTRAINT "_ClusterToEnvironment_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToEnvironment" ADD CONSTRAINT "_ClusterToEnvironment_B_fkey" FOREIGN KEY ("B") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_B_fkey" FOREIGN KEY ("B") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230711132934_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230711132934_dso/migration.sql new file mode 100644 index 000000000..8f3fb5ff9 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230711132934_dso/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `parentClusterId` on the `Kubeconfig` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "Kubeconfig_parentClusterId_key"; + +-- AlterTable +ALTER TABLE "Kubeconfig" DROP COLUMN "parentClusterId"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230802143822_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230802143822_dso/migration.sql new file mode 100644 index 000000000..4eb37edc5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230802143822_dso/migration.sql @@ -0,0 +1,10 @@ +CREATE TYPE "ProjectStatus" AS ENUM ('initializing', 'created', 'failed', 'archived'); + +ALTER TABLE public."Project" ALTER COLUMN status TYPE "ProjectStatus" USING + case + when status = 'created' then 'created'::"ProjectStatus" + when status = 'failed' then 'failed'::"ProjectStatus" + when status = 'archived' then 'archived'::"ProjectStatus" + else 'initializing'::"ProjectStatus" + end; +ALTER TABLE public."Project" ALTER COLUMN status SET DEFAULT 'initializing'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230912084459_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230912084459_dso/migration.sql new file mode 100644 index 000000000..f402a0e3d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230912084459_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Cluster" ADD COLUMN "infos" VARCHAR(200); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231010111515_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231010111515_dso/migration.sql new file mode 100644 index 000000000..f553e7880 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231010111515_dso/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `externalToken` on the `Repository` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Repository" DROP COLUMN "externalToken"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125838_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125838_dso/migration.sql new file mode 100644 index 000000000..394b650a5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125838_dso/migration.sql @@ -0,0 +1,81 @@ +-- CreateEnum +CREATE TYPE "QuotaStageStatus" AS ENUM +('active', 'pendingDelete'); + +-- Create new tables +-- CreateTable +CREATE TABLE "Quota" +( + "id" UUID NOT NULL, + "memory" VARCHAR NOT NULL, + "cpu" REAL NOT NULL, + "name" VARCHAR NOT NULL, + "isPrivate" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "Quota_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Stage" +( + "id" UUID NOT NULL, + "name" VARCHAR NOT NULL, + + CONSTRAINT "Stage_pkey" PRIMARY KEY ("id") +); + +-- Associate Quotas and Stages +-- CreateTable +CREATE TABLE "QuotaStage" +( + "id" UUID NOT NULL, + "quotaId" UUID NOT NULL, + "stageId" UUID NOT NULL, + "status" "QuotaStageStatus" NOT NULL DEFAULT 'active', + + CONSTRAINT "QuotaStage_pkey" PRIMARY KEY ("id") +); +CREATE UNIQUE INDEX "Quota_id_key" ON "Quota"("id"); +CREATE UNIQUE INDEX "Quota_name_key" ON "Quota"("name"); +CREATE UNIQUE INDEX "Stage_id_key" ON "Stage"("id"); +CREATE UNIQUE INDEX "Stage_name_key" ON "Stage"("name"); +CREATE UNIQUE INDEX "QuotaStage_id_key" ON "QuotaStage"("id"); +CREATE UNIQUE INDEX "QuotaStage_quotaId_stageId_key" ON "QuotaStage"("quotaId", "stageId"); +ALTER TABLE "QuotaStage" ADD CONSTRAINT "QuotaStage_quotaId_fkey" FOREIGN KEY ("quotaId") REFERENCES "Quota"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "QuotaStage" ADD CONSTRAINT "QuotaStage_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- Create default values for Quotas and Stages +-- Quota +INSERT INTO "Quota" + (id, cpu, memory, "name", "isPrivate") +VALUES + ('5a57b62f-2465-4fb6-a853-5a751d099199', 2, '4Gi', 'small', false), + ('08770663-3b76-4af6-8978-9f75eda4faa7', 4, '8Gi', 'medium', false), + ('b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', 6, '12Gi', 'large', false), + ('97b851e8-9067-4a3d-a0e8-c3a6820c49be', 8, '16Gi', 'xlarge', false); + +-- Stage +INSERT INTO "Stage" + (id, "name") +VALUES + ('4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', 'dev'), + ('38fa869d-6267-441d-af7f-e0548fd06b7e', 'staging'), + ('d434310e-7850-4d59-b47f-0772edf50582', 'integration'), + ('9b3e9991-896d-4d90-bdc5-a34be8c06b8f', 'prod'); + +-- QuotaStage +INSERT INTO "QuotaStage" + (id, "quotaId", "stageId") +VALUES + ('0cb0c549-560e-4f26-8f4e-832dd722f68a', '5a57b62f-2465-4fb6-a853-5a751d099199', '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), + ('0530e9c9-b37d-4dec-93e6-1895f700e61c', '5a57b62f-2465-4fb6-a853-5a751d099199', '38fa869d-6267-441d-af7f-e0548fd06b7e'), + ('8a99db49-b7b1-44bf-865d-5e709e8aa0fc', '5a57b62f-2465-4fb6-a853-5a751d099199', 'd434310e-7850-4d59-b47f-0772edf50582'), + ('67561f00-d219-4ca6-b94a-3ee83f09d2d6', '5a57b62f-2465-4fb6-a853-5a751d099199', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), + ('8b3c201e-7518-4254-a94a-16c404e46936', '08770663-3b76-4af6-8978-9f75eda4faa7', '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), + ('9157ae12-3e39-43f8-a24f-ae5d9c6b69b7', '08770663-3b76-4af6-8978-9f75eda4faa7', '38fa869d-6267-441d-af7f-e0548fd06b7e'), + ('c733a1dd-c9fd-4def-b29e-df49ef7b6698', '08770663-3b76-4af6-8978-9f75eda4faa7', 'd434310e-7850-4d59-b47f-0772edf50582'), + ('15a51f47-0ab2-4a94-a808-722639d8c092', '08770663-3b76-4af6-8978-9f75eda4faa7', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), + ('cb66e80c-2304-472d-bc19-a411011674ca', 'b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', 'd434310e-7850-4d59-b47f-0772edf50582'), + ('59fb0e79-3a76-4b96-81d4-63f4caa98cfa', 'b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), + ('4174b22c-2bee-4f4a-9d85-da7b5463f214', '97b851e8-9067-4a3d-a0e8-c3a6820c49be', 'd434310e-7850-4d59-b47f-0772edf50582'), + ('de0589b6-7cf5-4f1e-ab44-53e71a6cdb7a', '97b851e8-9067-4a3d-a0e8-c3a6820c49be', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125839_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125839_dso/migration.sql new file mode 100644 index 000000000..8c98a7f74 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125839_dso/migration.sql @@ -0,0 +1,35 @@ +-- Multiplication des environnements par clusteurs +ALTER TABLE "Environment" ADD COLUMN "clusterId" UUID; + +DO +$$ +DECLARE + perm record; + cte record; + env_uuid UUID; +BEGIN + FOR cte IN SELECT "B" AS environmentId, "A" AS "clusterId", "name", "projectId", status, "updatedAt", "createdAt" + FROM public."_ClusterToEnvironment", public."Environment" + WHERE public."Environment".id = "B" + LOOP + env_uuid := gen_random_uuid(); + INSERT INTO public."Environment" (id, "name", "projectId", "clusterId", status, "createdAt", "updatedAt") VALUES + (env_uuid, cte."name", cte."projectId", cte."clusterId", cte.status, cte."createdAt", cte."updatedAt"); + + FOR perm in SELECT * FROM public."Permission" WHERE "environmentId" = cte.environmentId + LOOP + INSERT INTO public."Permission" (id, "level", "createdAt", "updatedAt", "environmentId", "userId") VALUES + (gen_random_uuid(), perm."level", perm."createdAt", perm."updatedAt", env_uuid, perm."userId"); + END LOOP; + END LOOP; +END; +$$ +; +DELETE FROM public."Environment" WHERE "clusterId" is null; +ALTER TABLE public."Environment" ALTER COLUMN "clusterId" SET NOT NULL; +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_clusterId_fkey" FOREIGN KEY ("clusterId") REFERENCES "Cluster"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Delete old _ClusterToEnvironment +ALTER TABLE "_ClusterToEnvironment" DROP CONSTRAINT "_ClusterToEnvironment_A_fkey"; +ALTER TABLE "_ClusterToEnvironment" DROP CONSTRAINT "_ClusterToEnvironment_B_fkey"; +DROP TABLE "_ClusterToEnvironment"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125841_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125841_dso/migration.sql new file mode 100644 index 000000000..035cd1c85 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125841_dso/migration.sql @@ -0,0 +1,36 @@ +-- Associate cluster to Stages +-- CreateTable +CREATE TABLE "_ClusterToStage" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); +-- AddForeignKey +ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_B_fkey" FOREIGN KEY ("B") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- CreateIndex +CREATE UNIQUE INDEX "_ClusterToStage_AB_unique" ON "_ClusterToStage"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ClusterToStage_B_index" ON "_ClusterToStage"("B"); + +DO +$$ +DECLARE + cluster record; + cte record; + env_uuid UUID; +BEGIN + FOR cluster IN SELECT id + FROM public."Cluster" + LOOP + INSERT INTO public."_ClusterToStage" ("A", "B") VALUES + (cluster.id, '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), + (cluster.id, '38fa869d-6267-441d-af7f-e0548fd06b7e'), + (cluster.id, 'd434310e-7850-4d59-b47f-0772edf50582'), + (cluster.id, '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'); + END LOOP; +END; +$$ diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231012105520_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231012105520_dso/migration.sql new file mode 100644 index 000000000..43793bdb4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231012105520_dso/migration.sql @@ -0,0 +1,11 @@ +-- AlterTable +ALTER TABLE "Environment" ADD COLUMN "quotaStageId" UUID; +UPDATE "Environment" SET "quotaStageId" = '8b3c201e-7518-4254-a94a-16c404e46936' WHERE "name" = 'dev'; +UPDATE "Environment" SET "quotaStageId" = '9157ae12-3e39-43f8-a24f-ae5d9c6b69b7' WHERE "name" = 'staging'; +UPDATE "Environment" SET "quotaStageId" = '4174b22c-2bee-4f4a-9d85-da7b5463f214' WHERE "name" = 'integration'; +UPDATE "Environment" SET "quotaStageId" = 'de0589b6-7cf5-4f1e-ab44-53e71a6cdb7a' WHERE "name" = 'prod'; +ALTER TABLE "Environment" ALTER COLUMN "name" SET DATA TYPE VARCHAR(11); +ALTER TABLE "Environment" ALTER COLUMN "quotaStageId" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_quotaStageId_fkey" FOREIGN KEY ("quotaStageId") REFERENCES "QuotaStage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231024155020_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231024155020_dso/migration.sql new file mode 100644 index 000000000..9af004b6a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231024155020_dso/migration.sql @@ -0,0 +1,3 @@ +-- Please read 6.0.0 Release notes ! +-- lock all projects +UPDATE public."Project" SET "locked"=true \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231026150220_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231026150220_dso/migration.sql new file mode 100644 index 000000000..d970d3965 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231026150220_dso/migration.sql @@ -0,0 +1,3 @@ +-- Please read 6.0.0 Release notes ! +-- set all projects to failed to avoid unlock them +UPDATE public."Project" SET "status" = 'failed' WHERE "status" != 'archived' \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240112135751_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240112135751_dso/migration.sql new file mode 100644 index 000000000..c387d9885 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240112135751_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Log" ADD COLUMN "requestId" VARCHAR(21); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240321123436_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240321123436_dso/migration.sql new file mode 100644 index 000000000..18e20262c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240321123436_dso/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `status` on the `Environment` table. All the data in the column will be lost. + - You are about to drop the column `status` on the `Repository` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Environment" DROP COLUMN "status"; + +-- AlterTable +ALTER TABLE "Repository" DROP COLUMN "status"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240329172938_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240329172938_dso/migration.sql new file mode 100644 index 000000000..f784b2156 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240329172938_dso/migration.sql @@ -0,0 +1,46 @@ +-- AlterTable +ALTER TABLE "Cluster" ADD COLUMN "zoneId" UUID; + +-- CreateTable +CREATE TABLE "Zone" +( + "id" UUID NOT NULL, + "slug" VARCHAR(10) NOT NULL, + "label" VARCHAR(50) NOT NULL, + "description" VARCHAR(200), + "createdAt" TIMESTAMP +(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP +(3) NOT NULL, + + CONSTRAINT "Zone_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Zone_id_key" ON "Zone"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Zone_slug_key" ON "Zone"("slug"); + +-- Create default zone +INSERT INTO "Zone" + (id, "slug", "label", "description", "updatedAt") +VALUES + ('a66c4230-eba6-41f1-aae5-bb1e4f90cce0', 'default', 'Zone Défaut', 'Zone par défaut, à changer', CURRENT_TIMESTAMP); + +-- Set default zoneId for current clusters +UPDATE "Cluster" +SET "zoneId" += 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0' +WHERE "zoneId" +IS NULL; + +-- AlterTable +ALTER TABLE "Cluster" ALTER COLUMN "zoneId" +SET +NOT NULL; + +-- AddForeignKey +ALTER TABLE "Cluster" ADD CONSTRAINT "Cluster_zoneId_fkey" FOREIGN KEY ("zoneId") REFERENCES "Zone"("id") +ON DELETE RESTRICT ON +UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240424093852_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240424093852_dso/migration.sql new file mode 100644 index 000000000..cb4f7ad96 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240424093852_dso/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "ProjectPlugin" ( + "pluginName" TEXT NOT NULL, + "projectId" UUID NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "AdminPlugin" ( + "pluginName" TEXT NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectPlugin_projectId_pluginName_key_key" ON "ProjectPlugin"("projectId", "pluginName", "key"); + +-- CreateIndex +CREATE UNIQUE INDEX "AdminPlugin_pluginName_key_key" ON "AdminPlugin"("pluginName", "key"); + +-- AddForeignKey +ALTER TABLE "ProjectPlugin" ADD CONSTRAINT "ProjectPlugin_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240427181037_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240427181037_dso/migration.sql new file mode 100644 index 000000000..11672324f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240427181037_dso/migration.sql @@ -0,0 +1,19 @@ +DO $$ +DECLARE + project_row RECORD; + registry_id INT; +BEGIN + -- Début de la boucle sur chaque ligne de la table 'Project' + FOR project_row IN SELECT id, services FROM public."Project" LOOP + -- Extrait 'registry.id' de la colonne JSON 'services' + registry_id := (SELECT (project_row.services -> 'registry' ->> 'id')::TEXT); + -- Si 'registry.id' existe, insérer dans la table 'config' + IF registry_id IS NOT NULL THEN + INSERT INTO public."ProjectPlugin" ("projectId", "pluginName", "key", "value") + VALUES (project_row.id, 'registry', 'projectId', registry_id::TEXT); + END IF; + END LOOP; +END $$; + +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "services"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240605135052_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240605135052_dso/migration.sql new file mode 100644 index 000000000..9c7d5a8f8 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240605135052_dso/migration.sql @@ -0,0 +1,9 @@ +-- CreateEnum +CREATE TYPE "RoleList" AS ENUM ('owner', 'user'); + +-- AlterTable +ALTER TABLE public."Role" ALTER COLUMN "role" TYPE "RoleList" USING + case + when role = 'owner' then 'owner'::"RoleList" + else 'user'::"RoleList" + end; \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240612123132_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240612123132_dso/migration.sql new file mode 100644 index 000000000..45a4a5d1e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240612123132_dso/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "ProjectClusterHistory" ( + "projectId" UUID NOT NULL, + "clusterId" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectClusterHistory_projectId_clusterId_key" ON "ProjectClusterHistory"("projectId", "clusterId"); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240614222908_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240614222908_dso/migration.sql new file mode 100644 index 000000000..2b1641a65 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240614222908_dso/migration.sql @@ -0,0 +1,11 @@ +DO $$ +DECLARE + env_row RECORD; +BEGIN + -- Début de la boucle sur chaque ligne de la table 'Project' + FOR env_row IN SELECT "projectId", "clusterId" FROM public."Environment" LOOP + INSERT INTO public."ProjectClusterHistory" ("projectId", "clusterId") + VALUES (env_row."projectId", env_row."clusterId") + ON CONFLICT DO NOTHING; + END LOOP; +END $$; \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240618112205_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240618112205_dso/migration.sql new file mode 100644 index 000000000..5e7ff03d4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240618112205_dso/migration.sql @@ -0,0 +1,58 @@ +-- AlterTable +ALTER TABLE "Environment" ADD COLUMN "quotaId" UUID, +ADD COLUMN "stageId" UUID; + +-- AddForeignKey +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_quotaId_fkey" FOREIGN KEY ("quotaId") REFERENCES "Quota"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE "_QuotaToStage" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_QuotaToStage_AB_unique" ON "_QuotaToStage"("A", "B"); + +-- CreateIndex +CREATE INDEX "_QuotaToStage_B_index" ON "_QuotaToStage"("B"); + +-- AddForeignKey +ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_A_fkey" FOREIGN KEY ("A") REFERENCES "Quota"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_B_fkey" FOREIGN KEY ("B") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +DO $$ +DECLARE + quota_stage_row RECORD; +BEGIN + FOR quota_stage_row IN SELECT * FROM public."QuotaStage" loop + UPDATE public."Environment" SET "stageId" = quota_stage_row."stageId" WHERE "Environment"."quotaStageId" = quota_stage_row.id; + UPDATE public."Environment" SET "quotaId" = quota_stage_row."quotaId" WHERE "Environment"."quotaStageId" = quota_stage_row.id; + insert into public."_QuotaToStage" values (quota_stage_row."quotaId", quota_stage_row."stageId"); + END LOOP; +END $$; + +-- DropForeignKey +ALTER TABLE "Environment" DROP CONSTRAINT "Environment_quotaStageId_fkey"; + +-- AlterTable +ALTER TABLE "Environment" ALTER COLUMN "quotaId" SET NOT NULL, +ALTER COLUMN "stageId" SET NOT NULL, +DROP COLUMN "quotaStageId"; + +-- DropForeignKey +ALTER TABLE "QuotaStage" DROP CONSTRAINT "QuotaStage_quotaId_fkey"; + +-- DropForeignKey +ALTER TABLE "QuotaStage" DROP CONSTRAINT "QuotaStage_stageId_fkey"; + +-- DropTable +DROP TABLE "QuotaStage"; + +-- DropEnum +DROP TYPE "QuotaStageStatus"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240717084709_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240717084709_dso/migration.sql new file mode 100644 index 000000000..0036da8a1 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240717084709_dso/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Made the column `description` on table `Project` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Project" ALTER COLUMN "description" SET NOT NULL, +ALTER COLUMN "description" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240723135420_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240723135420_dso/migration.sql new file mode 100644 index 000000000..ed6ae9b84 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240723135420_dso/migration.sql @@ -0,0 +1,198 @@ +-- DropForeignKey if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Permission_environmentId_fkey') THEN + ALTER TABLE "Permission" DROP CONSTRAINT "Permission_environmentId_fkey"; + END IF; +END $$; + +-- DropForeignKey if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Permission_userId_fkey') THEN + ALTER TABLE "Permission" DROP CONSTRAINT "Permission_userId_fkey"; + END IF; +END $$; + +-- DropForeignKey if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Role_projectId_fkey') THEN + ALTER TABLE "Role" DROP CONSTRAINT "Role_projectId_fkey"; + END IF; +END $$; + +-- DropForeignKey if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Role_userId_fkey') THEN + ALTER TABLE "Role" DROP CONSTRAINT "Role_userId_fkey"; + END IF; +END $$; + +-- CreateTable if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ProjectMembers') THEN + CREATE TABLE "ProjectMembers" ( + "projectId" UUID NOT NULL, + "userId" UUID NOT NULL, + "roleIds" TEXT[] + ); + END IF; +END $$; + +-- AlterTable +ALTER TABLE "Log" ADD COLUMN IF NOT EXISTS "projectId" UUID; + +INSERT INTO public."User" (id, "firstName", "lastName", email, "createdAt", "updatedAt") +VALUES('04ac168a-2c4f-4816-9cce-af6c612e5912'::uuid, 'Anonymous', 'User', 'anon@user', '2023-07-03 14:46:56.770', '2023-07-03 14:46:56.770') +ON CONFLICT (id) DO NOTHING; + +-- AlterTable +ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "everyonePerms" BIGINT NOT NULL DEFAULT 896, +ADD COLUMN IF NOT EXISTS "ownerId" UUID; + +DO $$ +DECLARE + role_row RECORD; +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Role') THEN + -- Début de la boucle sur chaque ligne de la table 'Project' + FOR role_row IN SELECT "userId", "projectId", "role" FROM public."Role" LOOP + INSERT INTO public."ProjectMembers" ("userId", "projectId", "roleIds") VALUES (role_row."userId", role_row."projectId", '{}'); + IF role_row."role" = 'owner'::public."RoleList" THEN + UPDATE public."Project" + SET "ownerId"=role_row."userId" + WHERE id=role_row."projectId"::uuid; + END IF; + END LOOP; + END IF; +END $$; + +UPDATE public."Project" +SET "ownerId"='04ac168a-2c4f-4816-9cce-af6c612e5912' +WHERE "ownerId" IS NULL; + +ALTER TABLE public."Project" ALTER COLUMN "ownerId" SET NOT NULL; + +DELETE FROM public."ProjectMembers" pm +USING public."Project" p +WHERE pm."userId" = p."ownerId" +AND pm."projectId" = p."id"; + +-- DropTable if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Permission') THEN + DROP TABLE "Permission"; + END IF; +END $$; + +-- DropTable if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Role') THEN + DROP TABLE "Role"; + END IF; +END $$; + +-- DropEnum if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'RoleList') THEN + DROP TYPE "RoleList"; + END IF; +END $$; + +-- CreateTable if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'AdminRole') THEN + CREATE TABLE "AdminRole" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "permissions" BIGINT NOT NULL, + "position" SMALLINT NOT NULL, + CONSTRAINT "AdminRole_pkey" PRIMARY KEY ("id") + ); + END IF; +END $$; + +-- CreateTable if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ProjectRole') THEN + CREATE TABLE "ProjectRole" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "permissions" BIGINT NOT NULL, + "projectId" UUID NOT NULL, + "position" SMALLINT NOT NULL, + CONSTRAINT "ProjectRole_pkey" PRIMARY KEY ("id") + ); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'AdminRole_id_key') THEN + CREATE UNIQUE INDEX "AdminRole_id_key" ON "AdminRole"("id"); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'AdminRole_name_key') THEN + CREATE UNIQUE INDEX "AdminRole_name_key" ON "AdminRole"("name"); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'ProjectMembers_projectId_userId_key') THEN + CREATE UNIQUE INDEX "ProjectMembers_projectId_userId_key" ON "ProjectMembers"("projectId", "userId"); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'ProjectRole_id_key') THEN + CREATE UNIQUE INDEX "ProjectRole_id_key" ON "ProjectRole"("id"); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'Environment_projectId_name_key') THEN + CREATE UNIQUE INDEX "Environment_projectId_name_key" ON "Environment"("projectId", "name"); + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Log_projectId_fkey') THEN + ALTER TABLE "Log" ADD CONSTRAINT "Log_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Project_ownerId_fkey') THEN + ALTER TABLE "Project" ADD CONSTRAINT "Project_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectMembers_projectId_fkey') THEN + ALTER TABLE "ProjectMembers" ADD CONSTRAINT "ProjectMembers_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectMembers_userId_fkey') THEN + ALTER TABLE "ProjectMembers" ADD CONSTRAINT "ProjectMembers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectRole_projectId_fkey') THEN + ALTER TABLE "ProjectRole" ADD CONSTRAINT "ProjectRole_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "adminRoleIds" TEXT[]; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240725162050_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240725162050_dso/migration.sql new file mode 100644 index 000000000..c9b41827b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240725162050_dso/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "AdminRole_name_key"; + +-- AlterTable +ALTER TABLE "AdminRole" ADD COLUMN "oidcGroup" TEXT; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240726210139_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240726210139_dso/migration.sql new file mode 100644 index 000000000..265f262ab --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240726210139_dso/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - Made the column `oidcGroup` on table `AdminRole` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable + +UPDATE public."AdminRole" +SET "oidcGroup"='' +WHERE "oidcGroup" IS NULL; + +ALTER TABLE "AdminRole" ALTER COLUMN "oidcGroup" SET NOT NULL, +ALTER COLUMN "oidcGroup" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240808082632_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240808082632_dso/migration.sql new file mode 100644 index 000000000..4fc276860 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240808082632_dso/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "SystemSetting" +( + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + + CONSTRAINT "SystemSetting_pkey" PRIMARY KEY ("key") +); + +-- CreateIndex +CREATE UNIQUE INDEX "SystemSetting_key_key" ON "SystemSetting"("key"); + +-- Create maintenance setting +INSERT INTO "SystemSetting" + ("key", "value") +VALUES + ('maintenance', 'off'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240826143230_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240826143230_dso/migration.sql new file mode 100644 index 000000000..95ab54869 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240826143230_dso/migration.sql @@ -0,0 +1,3 @@ +INSERT INTO public."AdminRole" +(id, "name", permissions, "position", "oidcGroup") +VALUES('76229c96-4716-45bc-99da-00498ec9018c'::uuid, 'Admin', 2, 0, '/admin'); \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240829085548_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240829085548_dso/migration.sql new file mode 100644 index 000000000..c11648218 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240829085548_dso/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Made the column `externalUserName` on table `Repository` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +UPDATE "Repository" SET "externalUserName" = '' WHERE "externalUserName" IS NULL; + +ALTER TABLE "Repository" ALTER COLUMN "externalUserName" SET NOT NULL, +ALTER COLUMN "externalUserName" SET DEFAULT '', +ALTER COLUMN "externalRepoUrl" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240916141253_token/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240916141253_token/migration.sql new file mode 100644 index 000000000..b0472cd80 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240916141253_token/migration.sql @@ -0,0 +1,23 @@ +-- CreateEnum +CREATE TYPE "TokenStatus" AS ENUM ('active', 'revoked'); + +-- CreateTable +CREATE TABLE "AdminToken" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "permissions" BIGINT NOT NULL, + "userId" UUID, + "expirationDate" TIMESTAMP(3), + "lastUse" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" "TokenStatus" NOT NULL DEFAULT 'active', + "hash" TEXT NOT NULL, + + CONSTRAINT "AdminToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AdminToken_id_key" ON "AdminToken"("id"); + +-- AddForeignKey +ALTER TABLE "AdminToken" ADD CONSTRAINT "AdminToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240919122331_optional_user_id/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240919122331_optional_user_id/migration.sql new file mode 100644 index 000000000..47488b00c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240919122331_optional_user_id/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Log" DROP CONSTRAINT "Log_userId_fkey"; + +-- AlterTable +ALTER TABLE "Log" ALTER COLUMN "userId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Log" ADD CONSTRAINT "Log_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923142722_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923142722_dso/migration.sql new file mode 100644 index 000000000..18eca3ead --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923142722_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Log" ALTER COLUMN "requestId" SET DATA TYPE VARCHAR(36); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923155416_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923155416_dso/migration.sql new file mode 100644 index 000000000..74e0946f0 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923155416_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Zone" ADD COLUMN "argocdUrl" TEXT NOT NULL DEFAULT 'https://example.com'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240928002900_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240928002900_dso/migration.sql new file mode 100644 index 000000000..41dac7535 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240928002900_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "ProjectStatus" ADD VALUE 'warning'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241008125724_enabling_maven/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241008125724_enabling_maven/migration.sql new file mode 100644 index 000000000..ef888d5e5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241008125724_enabling_maven/migration.sql @@ -0,0 +1,12 @@ +DO $$ +DECLARE + project_row RECORD; + registry_id INT; +BEGIN + -- Début de la boucle sur chaque ligne de la table 'Project' + FOR project_row IN SELECT id FROM public."Project" WHERE status <> 'archived'::public."ProjectStatus" LOOP + INSERT INTO public."ProjectPlugin" ("projectId", "pluginName", "key", "value") + VALUES (project_row.id, 'nexus', 'activateMavenRepo', 'enabled') + ON CONFLICT DO NOTHING; + END LOOP; +END $$; \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232540_add_usertype/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232540_add_usertype/migration.sql new file mode 100644 index 000000000..a57e5956c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232540_add_usertype/migration.sql @@ -0,0 +1,12 @@ +-- CreateEnum +CREATE TYPE "UserType" AS ENUM ('human', 'bot', 'ghost'); + +-- AlterEnum +ALTER TYPE "TokenStatus" ADD VALUE 'inactive'; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "type" "UserType" NOT NULL DEFAULT 'human'; +UPDATE "User" SET type = 'ghost' WHERE id = '04ac168a-2c4f-4816-9cce-af6c612e5912'; + +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "type" DROP DEFAULT; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232541_add_pat/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232541_add_pat/migration.sql new file mode 100644 index 000000000..71e15a312 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232541_add_pat/migration.sql @@ -0,0 +1,84 @@ +-- CreateTable (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'PersonalAccessToken') THEN + CREATE TABLE "PersonalAccessToken" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "userId" UUID NOT NULL, + "expirationDate" TIMESTAMP(3) NOT NULL, + "lastUse" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" "TokenStatus" NOT NULL DEFAULT 'active', + "hash" TEXT NOT NULL, + + CONSTRAINT "PersonalAccessToken_pkey" PRIMARY KEY ("id") + ); + END IF; +END $$; + +-- CreateIndex (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'PersonalAccessToken_id_key') THEN + CREATE UNIQUE INDEX "PersonalAccessToken_id_key" ON "PersonalAccessToken"("id"); + END IF; +END $$; + +-- AddForeignKey (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'PersonalAccessToken_userId_fkey') THEN + ALTER TABLE "PersonalAccessToken" ADD CONSTRAINT "PersonalAccessToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +-- Process AdminToken (idempotent) +DO $$ +DECLARE + admin_token record; + user_uuid UUID; +BEGIN + FOR admin_token IN SELECT "name", "id" + FROM public."AdminToken" + LOOP + -- Generate new UUID if user does not exist + user_uuid := COALESCE( + (SELECT id FROM public."User" WHERE email = concat(admin_token.name, '@bot.id')), + gen_random_uuid() + ); + + -- Insert user if not already exists + INSERT INTO public."User" (id, "firstName", "lastName", email, "createdAt", "updatedAt", "type") + VALUES(user_uuid, 'Bot Admin', admin_token.name, concat(admin_token.name, '@bot.id'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'bot') + ON CONFLICT (id) DO NOTHING; + + -- Update AdminToken with the new user ID + UPDATE public."AdminToken" SET "userId" = user_uuid WHERE id = admin_token.id; + END LOOP; +END $$; + +-- Alter AdminToken userId column to NOT NULL (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'AdminToken' AND column_name = 'userId' AND is_nullable = 'NO') THEN + ALTER TABLE public."AdminToken" ALTER COLUMN "userId" SET NOT NULL; + END IF; +END $$; + +-- DropForeignKey if exists (idempotent) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'AdminToken_userId_fkey') THEN + ALTER TABLE "AdminToken" DROP CONSTRAINT "AdminToken_userId_fkey"; + END IF; +END $$; + +-- AddForeignKey (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'AdminToken_userId_fkey') THEN + ALTER TABLE "AdminToken" ADD CONSTRAINT "AdminToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241107142721_user_last_login/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241107142721_user_last_login/migration.sql new file mode 100644 index 000000000..521b2b10a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241107142721_user_last_login/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "lastLogin" TIMESTAMP(3); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112101945_add_slug/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112101945_add_slug/migration.sql new file mode 100644 index 000000000..a7500833b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112101945_add_slug/migration.sql @@ -0,0 +1,14 @@ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "slug" TEXT; + +UPDATE public."Project" p +SET slug = ( + SELECT concat(org.name, '-', subp.name) FROM public."Project" subp + LEFT JOIN public."Organization" org on org."id" = subp."organizationId" + WHERE subp.id = p.id +); + +ALTER TABLE public."Project" ALTER COLUMN "slug" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug"); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql new file mode 100644 index 000000000..b143cbeb9 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "lastSuccessProvisionningVersion" TEXT; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241216131342_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241216131342_dso/migration.sql new file mode 100644 index 000000000..7a8868190 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241216131342_dso/migration.sql @@ -0,0 +1,17 @@ +-- AlterTable +ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "_ClusterToProject_AB_unique"; + +-- AlterTable +ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "_ClusterToStage_AB_unique"; + +-- AlterTable +ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "_QuotaToStage_AB_unique"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250107104749_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250107104749_dso/migration.sql new file mode 100644 index 000000000..21ce77b8d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250107104749_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Cluster" ADD COLUMN "external" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql new file mode 100644 index 000000000..ac63cd639 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql @@ -0,0 +1,25 @@ +-- Vérifie les versions dans la table Project +DO $$ +DECLARE + project_id TEXT; + project_name TEXT; + last_version TEXT; +BEGIN + -- Boucle sur les projets non archivés + FOR project_id, project_name, last_version IN ( + SELECT id, name, "lastSuccessProvisionningVersion" + FROM "Project" + WHERE "status" != 'archived' + ) + LOOP + -- Vérifie si la version est NULL + IF last_version IS NULL THEN + RAISE EXCEPTION 'Le projet % (ID: %) a une version NULL.', project_name, project_id; + END IF; + + -- Vérifie si la version est inférieure à 8.23.0 selon SemVer + IF (string_to_array(last_version, '.')::int[] < ARRAY[8,23,0]) THEN + RAISE EXCEPTION 'Le projet % (ID: %) a une version (%), inférieure à 8.23.0.', project_name, project_id, last_version; + END IF; + END LOOP; +END $$; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222954_drop_organization/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222954_drop_organization/migration.sql new file mode 100644 index 000000000..54871c901 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222954_drop_organization/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - You are about to drop the column `organizationId` on the `Project` table. All the data in the column will be lost. + - You are about to drop the `Organization` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Project" DROP CONSTRAINT "Project_organizationId_fkey"; + +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "organizationId"; + +-- DropTable +DROP TABLE "Organization"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250723141246_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250723141246_dso/migration.sql new file mode 100644 index 000000000..68ca0df2f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250723141246_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Cluster" ALTER COLUMN "infos" SET DATA TYPE VARCHAR(1000); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250818095032_remove_quota/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250818095032_remove_quota/migration.sql new file mode 100644 index 000000000..8364090d8 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250818095032_remove_quota/migration.sql @@ -0,0 +1,44 @@ +-- AlterTable +ALTER TABLE "Environment" +ADD COLUMN "cpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "gpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "memory" REAL NOT NULL DEFAULT 0; + +COMMENT ON COLUMN "Environment".cpu IS 'CPU share as float (1 and 0.01 are valid values)'; +COMMENT ON COLUMN "Environment".gpu IS 'GPU share as float (1 and 0.01 are valid values)'; +COMMENT ON COLUMN "Environment".memory IS 'Memory value as GigaBytes (1 and 0.01 are valid values)'; + +-- Use values from Quota. Memory is an extract of q.memory numeric value as it contains a unit (e.g. '2Gi'). +UPDATE "Environment" +SET cpu = q.cpu, memory = COALESCE(NULLIF(regexp_replace(q.memory, '\D', '','g'), ''), '0')::numeric +FROM "Quota" q +WHERE "quotaId" = q."id"; + +/* + Warnings: + + - You are about to drop the column `quotaId` on the `Environment` table. All the data in the column will be lost. + - You are about to drop the `Quota` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_QuotaToStage` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Environment" DROP CONSTRAINT "Environment_quotaId_fkey"; + +-- DropForeignKey +ALTER TABLE "_QuotaToStage" DROP CONSTRAINT "_QuotaToStage_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_QuotaToStage" DROP CONSTRAINT "_QuotaToStage_B_fkey"; + +-- AlterTable +ALTER TABLE "Environment" DROP COLUMN "quotaId", +ALTER COLUMN "cpu" DROP DEFAULT, +ALTER COLUMN "gpu" DROP DEFAULT, +ALTER COLUMN "memory" DROP DEFAULT; + +-- DropTable +DROP TABLE "Quota"; + +-- DropTable +DROP TABLE "_QuotaToStage"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql new file mode 100644 index 000000000..77f32b5ab --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Cluster" +ADD COLUMN "cpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "gpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "memory" REAL NOT NULL DEFAULT 0; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250916134454_add_project_resources/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250916134454_add_project_resources/migration.sql new file mode 100644 index 000000000..decca804a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250916134454_add_project_resources/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "Project" +ADD COLUMN "limitless" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "hprodCpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "hprodGpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "hprodMemory" REAL NOT NULL DEFAULT 0, +ADD COLUMN "prodCpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "prodGpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "prodMemory" REAL NOT NULL DEFAULT 0; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql new file mode 100644 index 000000000..95f3a689d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql @@ -0,0 +1,4 @@ +-- Rename default zone +UPDATE "Zone" +SET ("label", "description") = ('DSO', 'Zone par défaut') +WHERE slug = 'default'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/migration_lock.toml b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..648c57fd5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/admin.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/admin.prisma new file mode 100644 index 000000000..71cfb1754 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/admin.prisma @@ -0,0 +1,20 @@ +model AdminPlugin { + pluginName String + key String + value String + + @@unique([pluginName, key]) +} + +model AdminRole { + id String @id @unique @default(uuid()) @db.Uuid + name String + permissions BigInt + position Int @db.SmallInt + oidcGroup String @default("") +} + +model SystemSetting { + key String @id @unique + value String +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma new file mode 100644 index 000000000..44569a8fa --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma @@ -0,0 +1,103 @@ +model Environment { + id String @id @default(uuid()) @db.Uuid + name String @db.VarChar(11) + projectId String @db.Uuid + memory Float @db.Real + cpu Float @db.Real + gpu Float @db.Real + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + clusterId String @db.Uuid + stageId String @db.Uuid + cluster Cluster @relation(fields: [clusterId], references: [id]) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + stage Stage @relation(fields: [stageId], references: [id]) + + @@unique([projectId, name]) +} + +model Repository { + id String @id @default(uuid()) @db.Uuid + projectId String @db.Uuid + internalRepoName String + externalRepoUrl String @default("") + externalUserName String @default("") + isInfra Boolean @default(false) + isPrivate Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) +} + +model ProjectClusterHistory { + projectId String @db.Uuid + clusterId String @db.Uuid + + @@unique([projectId, clusterId]) +} + +model ProjectMembers { + projectId String @db.Uuid + userId String @db.Uuid + roleIds String[] + project Project @relation(fields: [projectId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@unique([projectId, userId]) +} + +model ProjectPlugin { + pluginName String + projectId String @db.Uuid + key String + value String + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@unique([projectId, pluginName, key]) +} + +model ProjectRole { + id String @id @unique @default(uuid()) @db.Uuid + name String + permissions BigInt + projectId String @db.Uuid + position Int @db.SmallInt + project Project @relation(fields: [projectId], references: [id]) +} + +model Project { + id String @id @unique @default(uuid()) @db.Uuid + name String + description String @default("") + status ProjectStatus @default(initializing) + locked Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + everyonePerms BigInt @default(896) + ownerId String @db.Uuid + environments Environment[] + logs Log[] + owner User @relation(fields: [ownerId], references: [id]) + members ProjectMembers[] + plugins ProjectPlugin[] + roles ProjectRole[] + repositories Repository[] + clusters Cluster[] @relation("ClusterToProject") + slug String @unique + limitless Boolean @default(true) + hprodCpu Float @db.Real + hprodGpu Float @db.Real + hprodMemory Float @db.Real + prodCpu Float @db.Real + prodGpu Float @db.Real + prodMemory Float @db.Real + lastSuccessProvisionningVersion String? +} + +enum ProjectStatus { + initializing + created + failed + archived + warning +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/schema.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/schema.prisma new file mode 100644 index 000000000..aadf7fea1 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/schema.prisma @@ -0,0 +1,21 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DB_URL") +} + +model Log { + id String @id @default(uuid()) @db.Uuid + data Json + action String @default("") + userId String? @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + requestId String? @db.VarChar(36) + projectId String? @db.Uuid + project Project? @relation(fields: [projectId], references: [id]) + user User? @relation(fields: [userId], references: [id]) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/token.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/token.prisma new file mode 100644 index 000000000..c0c55751c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/token.prisma @@ -0,0 +1,30 @@ +model AdminToken { + id String @id @unique @default(uuid()) @db.Uuid + name String + permissions BigInt + userId String @db.Uuid + expirationDate DateTime? + lastUse DateTime? + createdAt DateTime @default(now()) + status TokenStatus @default(active) + hash String + owner User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) +} + +model PersonalAccessToken { + id String @id @unique @default(uuid()) @db.Uuid + name String + userId String @db.Uuid + expirationDate DateTime + lastUse DateTime? + createdAt DateTime @default(now()) + status TokenStatus @default(active) + hash String + owner User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) +} + +enum TokenStatus { + active + revoked + inactive +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/topography.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/topography.prisma new file mode 100644 index 000000000..ad8e3be22 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/topography.prisma @@ -0,0 +1,53 @@ +model Cluster { + id String @id @unique @default(uuid()) @db.Uuid + label String @unique @db.VarChar(50) + privacy ClusterPrivacy @default(dedicated) + secretName String @unique @default(uuid()) @db.VarChar(50) + clusterResources Boolean @default(false) + kubeConfigId String @unique @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + infos String? @db.VarChar(1000) + external Boolean @default(false) + memory Float @db.Real + cpu Float @db.Real + gpu Float @db.Real + zoneId String @db.Uuid + kubeconfig Kubeconfig @relation(fields: [kubeConfigId], references: [id], onDelete: Cascade) + zone Zone @relation(fields: [zoneId], references: [id]) + environments Environment[] + projects Project[] @relation("ClusterToProject") + stages Stage[] @relation("ClusterToStage") +} + +model Kubeconfig { + id String @id @unique @default(uuid()) @db.Uuid + user Json + cluster Json + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + parentCluster Cluster? +} + +model Stage { + id String @id @unique @default(uuid()) @db.Uuid + name String @unique @db.VarChar + environments Environment[] + clusters Cluster[] @relation("ClusterToStage") +} + +model Zone { + id String @id @unique @default(uuid()) @db.Uuid + slug String @unique @db.VarChar(10) + label String @db.VarChar(50) + description String? @db.VarChar(200) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + argocdUrl String @default("https://example.com") + clusters Cluster[] +} + +enum ClusterPrivacy { + public + dedicated +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/user.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/user.prisma new file mode 100644 index 000000000..e90fb69f8 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/user.prisma @@ -0,0 +1,23 @@ +model User { + id String @id @db.Uuid + firstName String + lastName String + email String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastLogin DateTime? + adminRoleIds String[] + type UserType + + logs Log[] + projectsOwned Project[] + adminTokens AdminToken[] + projectMembers ProjectMembers[] + personalAccessTokens PersonalAccessToken[] +} + +enum UserType { + human + bot + ghost +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts new file mode 100644 index 000000000..9174f544e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts @@ -0,0 +1,183 @@ +import { describe, expect, it } from 'vitest' +import type { AdminRole, User } from '@prisma/client' +import { faker } from '@faker-js/faker' +import prisma from '../../__mocks__/prisma.js' +import { BadRequest400 } from '../../utils/errors.ts' +import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business.ts' + +describe('test admin-role business', () => { + describe('listRoles', () => { + it('should stringify bigint', async () => { + const partialRole: Partial = { + permissions: 4n, + } + + prisma.adminRole.findMany.mockResolvedValueOnce([partialRole]) + const response = await listRoles() + expect(response).toEqual([{ permissions: '4' }]) + }) + }) + + describe('createRole', () => { + it('should create role with incremented position when position 0 is the highest', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 0, + } + + prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole) + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.adminRole.create.mockResolvedValue(null) + await createRole({ name: 'test' }) + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 1 } }) + }) + + it('should create role with incremented position with bigger position', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + } + + prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole) + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.adminRole.create.mockResolvedValue(null) + await createRole({ name: 'test' }) + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 51 } }) + }) + + it('should create role with incremented position with no role in db', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + } + + prisma.adminRole.findFirst.mockResolvedValueOnce(undefined) + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.adminRole.create.mockResolvedValue(null) + await createRole({ name: 'test' }) + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 0 } }) + }) + }) + describe('deleteRole', () => { + const roleId = faker.string.uuid() + it('should delete role and remove id from concerned users', async () => { + const users = [{ + adminRoleIds: [roleId], + id: faker.string.uuid(), + }, { + adminRoleIds: [roleId, faker.string.uuid()], + id: faker.string.uuid(), + }] as const satisfies Partial[] + + prisma.user.findMany.mockResolvedValueOnce(users) + prisma.adminRole.findMany.mockResolvedValueOnce([]) + prisma.adminRole.create.mockResolvedValue(null) + await deleteRole(roleId) + + expect(prisma.user.update).toHaveBeenNthCalledWith(1, { where: { id: users[0].id }, data: { adminRoleIds: [] } }) + expect(prisma.user.update).toHaveBeenNthCalledWith(2, { where: { id: users[1].id }, data: { adminRoleIds: [users[1].adminRoleIds[1]] } }) + expect(prisma.adminRole.delete).toHaveBeenCalledWith({ where: { id: roleId } }) + }) + }) + describe('countRolesMembers', () => { + it('should return aggregated role member counts', async () => { + const partialRoles = [{ + id: faker.string.uuid(), + }, { + id: faker.string.uuid(), + }] as const satisfies Partial[] + + const users = [{ + adminRoleIds: [partialRoles[0].id, partialRoles[1].id], + }, { + adminRoleIds: [partialRoles[1].id], + }] as const satisfies Partial[] + prisma.adminRole.findMany.mockResolvedValue(partialRoles) + prisma.user.findMany.mockResolvedValue(users) + + const response = await countRolesMembers() + + expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 }) + }) + }) + describe('patchRoles', () => { + const dbRoles: AdminRole[] = [{ + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 0, + }, { + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 1, + }] + + it('should do nothing', async () => { + prisma.adminRole.findMany.mockResolvedValue([]) + await patchRoles([]) + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) + }) + + it('should return 400 if incoherent positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 1 }, + ] + prisma.adminRole.findMany.mockResolvedValue(dbRoles) + + const response = await patchRoles(updateRoles) + + expect(response).instanceOf(BadRequest400) + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) + }) + it('should return 400 if incoherent positions (missing roles)', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, position: 1 }, + ] + prisma.adminRole.findMany.mockResolvedValue(dbRoles) + + const response = await patchRoles(updateRoles) + + expect(response).instanceOf(BadRequest400) + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) + }) + it('should update positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 0 }, + ] + prisma.adminRole.findMany.mockResolvedValue(dbRoles) + + await patchRoles(updateRoles) + + expect(prisma.adminRole.update).toHaveBeenCalledTimes(2) + }) + it('should update permissions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, permissions: '0' }, + ] + prisma.adminRole.findMany.mockResolvedValue(dbRoles) + + await patchRoles(updateRoles) + + expect(prisma.adminRole.update).toHaveBeenCalledTimes(1) + expect(prisma.adminRole.update).toHaveBeenCalledWith({ + data: { + name: dbRoles[1].name, + oidcGroup: dbRoles[1].oidcGroup, + permissions: 0n, + position: 1, + }, + where: { + id: dbRoles[1].id, + }, + }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts new file mode 100644 index 000000000..a43cebf22 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts @@ -0,0 +1,90 @@ +import type { Project, ProjectRole } from '@prisma/client' +import type { AdminRole, adminRoleContract } from '@cpn-console/shared' +import { + listAdminRoles, +} from '@/resources/queries-index.js' +import type { ErrorResType } from '@/utils/errors.js' +import { BadRequest400 } from '@/utils/errors.js' +import prisma from '@/prisma.js' + +export async function listRoles() { + return listAdminRoles() + .then(roles => roles.map(role => ({ ...role, permissions: role.permissions.toString() }))) +} + +export async function patchRoles(roles: typeof adminRoleContract.patchAdminRoles.body._type): Promise { + const dbRoles = await prisma.adminRole.findMany() + const positionsAvailable: number[] = [] + + const updatedRoles: (Omit & { permissions: bigint })[] = dbRoles + .filter(dbRole => roles.find(role => role.id === dbRole.id)) // filter non concerned dbRoles + .map((dbRole) => { + const matchingRole = roles.find(role => role.id === dbRole.id) + if (typeof matchingRole?.position !== 'undefined' && !positionsAvailable.includes(matchingRole.position)) { + positionsAvailable.push(matchingRole.position) + } + return { + id: dbRole.id, + name: matchingRole?.name ?? dbRole.name, + permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : dbRole.permissions, + position: matchingRole?.position ?? dbRole.position, + oidcGroup: matchingRole?.oidcGroup ?? dbRole.oidcGroup, + } + }) + + if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes') + for (const { id, ...role } of updatedRoles) { + await prisma.adminRole.update({ where: { id }, data: role }) + } + + return listRoles() +} + +export async function createRole(role: typeof adminRoleContract.createAdminRole.body._type) { + const dbMaxPosRole = (await prisma.adminRole.findFirst({ + orderBy: { position: 'desc' }, + select: { position: true }, + }))?.position ?? -1 + + await prisma.adminRole.create({ + data: { + ...role, + position: dbMaxPosRole + 1, + permissions: 0n, + }, + }) + + return listRoles() +} + +export async function countRolesMembers() { + const roles = await prisma.adminRole.findMany({ where: { oidcGroup: { equals: '' } }, select: { id: true } }) + const roleIds = roles.map(role => role.id) + const users = await prisma.user.findMany({ + where: { adminRoleIds: { hasSome: roleIds } }, + select: { adminRoleIds: true }, + }) + const rolesCounts: Record = Object.fromEntries(roles.map(role => [role.id, 0])) // {role uuid: 0} + for (const { adminRoleIds } of users) { + for (const roleId of adminRoleIds) { + rolesCounts[roleId]++ + } + } + return rolesCounts +} + +export async function deleteRole(roleId: Project['id']) { + const allUsers = await prisma.user.findMany({ + where: { + adminRoleIds: { has: roleId }, + }, + }) + for (const user of allUsers) { + await prisma.user.update({ + where: { id: user.id }, + data: { adminRoleIds: user.adminRoleIds.filter(adminRoleId => adminRoleId !== roleId) }, + }) + } + await prisma.adminRole.delete({ where: { id: roleId } }) + return null +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts new file mode 100644 index 000000000..f4893b317 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts @@ -0,0 +1,32 @@ +import type { + AdminRole, + Prisma, +} from '@prisma/client' +import prisma from '@/prisma.js' + +export const listAdminRoles = () => prisma.adminRole.findMany({ orderBy: { position: 'asc' } }) + +export function createAdminRole(data: Pick) { + return prisma.adminRole.create({ + data: { + name: data.name, + permissions: 0n, + position: data.position, + }, + }) +} + +export function updateAdminRole(id: AdminRole['id'], data: Pick) { + return prisma.projectRole.updateMany({ + where: { id }, + data, + }) +} + +export function deleteAdminRole(id: AdminRole['id']) { + return prisma.projectRole.delete({ + where: { + id, + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts new file mode 100644 index 000000000..5fc0bc66c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts @@ -0,0 +1,181 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { adminRoleContract } from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { BadRequest400 } from '../../utils/errors.js' +import { getUserMockInfos } from '../../utils/mocks.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListRolesMock = vi.spyOn(business, 'listRoles') +const businessCreateRoleMock = vi.spyOn(business, 'createRole') +const businessPatchRolesMock = vi.spyOn(business, 'patchRoles') +const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers') +const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole') + +describe('test adminRoleContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listAdminRoles', () => { + it('should return list of admin roles', async () => { + const roles = [{ id: faker.string.uuid(), name: 'Role 1', oidcGroup: '', position: 0, permissions: '1' }] + businessListRolesMock.mockResolvedValueOnce(roles) + + const response = await app.inject() + .get(adminRoleContract.listAdminRoles.path) + .end() + + expect(businessListRolesMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(roles) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('createAdminRole', () => { + it('should create a role for authorized users', async () => { + const user = getUserMockInfos(true) + const newRole = { id: 'newRole', name: 'New Role' } + const roleData = { name: 'New Role' } + + authUserMock.mockResolvedValueOnce(user) + businessCreateRoleMock.mockResolvedValueOnce(newRole) + + const response = await app.inject() + .post(adminRoleContract.createAdminRole.path) + .body(roleData) + .end() + + expect(businessCreateRoleMock).toHaveBeenCalledWith(roleData) + expect(response.json()).toEqual(newRole) + expect(response.statusCode).toEqual(201) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(adminRoleContract.createAdminRole.path) + .body({ name: 'New Role' }) + .end() + + expect(businessCreateRoleMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('patchAdminRoles', () => { + const updatedRoles = [{ id: faker.string.uuid(), name: 'Role 1', oidcGroup: '', position: 0, permissions: '1' }] + const rolesData = [{ id: updatedRoles[0].id, name: 'Updated Role' }] + it('should update roles for authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessPatchRolesMock.mockResolvedValueOnce(updatedRoles) + + const response = await app.inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end() + + expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData) + expect(response.json()).toEqual(updatedRoles) + expect(response.statusCode).toEqual(200) + }) + + it('should return error if business logic fails', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessPatchRolesMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + + const response = await app.inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end() + + expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData) + expect(response.statusCode).toEqual(400) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end() + + expect(businessPatchRolesMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('adminRoleMemberCounts', () => { + it('should return counts of role members for admin', async () => { + const user = getUserMockInfos(true) + const counts = { role1: 5, role2: 3 } + + authUserMock.mockResolvedValueOnce(user) + businessCountRolesMembersMock.mockResolvedValueOnce(counts) + + const response = await app.inject() + .get(adminRoleContract.adminRoleMemberCounts.path) + .end() + + expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(counts) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 if user is not admin', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(adminRoleContract.adminRoleMemberCounts.path) + .end() + + expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('deleteAdminRole', () => { + const roleId = faker.string.uuid() + it('should delete a role for authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessDeleteRoleMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .delete(adminRoleContract.deleteAdminRole.path.replace(':roleId', roleId)) + .end() + + expect(businessDeleteRoleMock).toHaveBeenCalledWith(roleId) + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(adminRoleContract.deleteAdminRole.path.replace(':roleId', roleId)) + .end() + + expect(businessDeleteRoleMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts new file mode 100644 index 000000000..18b1fc226 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts @@ -0,0 +1,74 @@ +import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared' +import { + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@/utils/errors.js' + +export function adminRoleRouter() { + return serverInstance.router(adminRoleContract, { + // Récupérer des projets + listAdminRoles: async () => { + const body = await listRoles() + + return { + status: 200, + body, + } + }, + + createAdminRole: async ({ request: req, body }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await createRole(body) + + return { + status: 201, + body: resBody, + } + }, + + patchAdminRoles: async ({ request: req, body }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await patchRoles(body) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 200, + body: resBody, + } + }, + + adminRoleMemberCounts: async ({ request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await countRolesMembers() + + return { + status: 200, + body: resBody, + } + }, + + deleteAdminRole: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await deleteRole(params.roleId) + + return { + status: 204, + body: resBody, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts new file mode 100644 index 000000000..f9f338c64 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest' +import type { AdminToken } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import prisma from '../../__mocks__/prisma.js' +import { createToken, deleteToken, listTokens } from './business.ts' + +describe('test admin-token business', () => { + describe('listTokens', () => { + it('should stringify bigint', async () => { + const partialtoken: Partial = { + permissions: 4n, + } + + prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]) + const response = await listTokens({}) + expect(response).toEqual([{ permissions: '4' }]) + }) + it('should return revoked', async () => { + const partialtoken: Partial = { + permissions: 4n, + status: 'revoked', + } + + prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]) + const response = await listTokens({ withRevoked: true }) + expect(response).toEqual([{ ...partialtoken, permissions: '4' }]) + }) + }) + + describe('createToken', () => { + it('should create ', async () => { + const dbToken: Partial = undefined + const userId = faker.string.uuid() + const createdToken: AdminToken = { + expirationDate: null, + id: faker.string.uuid(), + name: 'test', + permissions: '2', + } + prisma.adminToken.findUnique.mockResolvedValueOnce(dbToken) + prisma.adminToken.create.mockResolvedValueOnce(createdToken) + await createToken({ name: 'test', permissions: '2', expirationDate: null }, userId, undefined) + + expect(prisma.adminToken.create).toHaveBeenCalledWith({ + data: { + name: 'test', + hash: expect.any(String), + permissions: 2n, + userId: expect.any(String), + expirationDate: undefined, + }, + omit: expect.any(Object), + include: { + owner: true, + }, + }) + }) + it('should not create cause expiration is too short', async () => { + const expirationDate = new Date() + await createToken({ name: 'test', permissions: '2', expirationDate: expirationDate.toISOString() }) + + expect(prisma.adminToken.create).toHaveBeenCalledTimes(0) + }) + }) + + describe('deleteToken', () => { + it('should delete token', async () => { + prisma.adminToken.delete.mockResolvedValueOnce(undefined) + await deleteToken(faker.string.uuid()) + expect(prisma.adminToken.updateMany).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts new file mode 100644 index 000000000..c5af30a43 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts @@ -0,0 +1,68 @@ +import { createHash, randomUUID } from 'node:crypto' +import { type adminTokenContract, generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' +import type { $Enums, AdminToken, Prisma } from '@prisma/client' +import prisma from '../../prisma.js' +import { BadRequest400 } from '@/utils/errors.js' + +export async function listTokens(query: typeof adminTokenContract.listAdminTokens.query._type) { + const where = { + status: { + in: ['active'] as $Enums.TokenStatus[], + }, + } as const satisfies Prisma.AdminTokenWhereInput + + if (query?.withRevoked) where.status.in.push('revoked') + + return prisma.adminToken.findMany({ + omit: { hash: true }, + include: { owner: true }, + orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], + where, + }).then(tokens => + tokens.map(({ permissions, ...token }) => ({ permissions: permissions.toString(), ...token })), + ) +} + +export async function createToken(data: typeof adminTokenContract.createAdminToken.body._type) { + if (data.expirationDate && !isAtLeastTomorrow(new Date(data.expirationDate))) { + return new BadRequest400('Date d\'expiration trop courte') + } + const password = generateRandomPassword(48, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') + const hash = createHash('sha256').update(password).digest('hex') + const botUserId = randomUUID() + await prisma.user.create({ + data: { + firstName: 'Bot Admin', + lastName: data.name, + type: 'bot', + id: botUserId, + email: `${botUserId}@bot.io`, + }, + }) + const token = await prisma.adminToken.create({ + data: { + ...data, + hash, + permissions: BigInt(data.permissions), + expirationDate: data.expirationDate ? new Date(data.expirationDate) : undefined, + userId: botUserId, + }, + omit: { hash: true }, + include: { owner: true }, + }) + return { + ...token, + password, + permissions: token.permissions.toString(), + } +} + +export async function deleteToken(id: AdminToken['id']) { + return prisma.adminToken.updateMany({ + where: { id }, + data: { + status: 'revoked', + expirationDate: new Date(Date.now()), + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts new file mode 100644 index 000000000..301cf6204 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts @@ -0,0 +1,161 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ExposedAdminToken } from '@cpn-console/shared' +import { adminTokenContract } from '@cpn-console/shared' +import type { AdminToken } from '@prisma/client' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getUserMockInfos } from '../../utils/mocks.js' +import { BadRequest400 } from '../../utils/errors.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListTokensMock = vi.spyOn(business, 'listTokens') +const businessCreateTokenMock = vi.spyOn(business, 'createToken') +const businessDeleteTokenMock = vi.spyOn(business, 'deleteToken') + +describe('test adminTokenContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listAdminTokens', () => { + it('should return list of admin tokens', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + const tokens: AdminToken[] = [{ + id: faker.string.uuid(), + name: 'token1', + permissions: '2', + lastUse: (new Date()).toISOString(), + expirationDate: null, + status: 'active', + createdAt: (new Date(Date.now())).toISOString(), + }] + businessListTokensMock.mockResolvedValueOnce(tokens) + + const response = await app.inject() + .get(adminTokenContract.listAdminTokens.path) + .end() + + expect(businessListTokensMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(tokens) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(adminTokenContract.listAdminTokens.path) + .end() + + expect(businessListTokensMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('createAdminToken', () => { + it('should create a token for authorized users', async () => { + const user = getUserMockInfos(true) + + const newToken = { + id: faker.string.uuid(), + name: 'test', + lastUse: null, + expirationDate: null, + password: faker.string.alpha({ casing: 'lower', length: 10 }), + permissions: '2', + createdAt: (new Date(Date.now())).toISOString(), + status: 'active', + } + const tokenData: ExposedAdminToken = { + name: newToken.name, + permissions: newToken.permissions, + expirationDate: null, + } + + authUserMock.mockResolvedValueOnce(user) + businessCreateTokenMock.mockResolvedValueOnce(newToken) + + const response = await app.inject() + .post(adminTokenContract.createAdminToken.path) + .body(tokenData) + .end() + + expect(businessCreateTokenMock).toHaveBeenCalledWith(tokenData) + expect(response.json()).toEqual(newToken) + expect(response.statusCode).toEqual(201) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(adminTokenContract.createAdminToken.path) + .body({ + name: 'new-token', + expirationDate: null, + permissions: '4', + }) + .end() + + expect(businessCreateTokenMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + + it('should pass business error', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessCreateTokenMock.mockResolvedValueOnce(new BadRequest400('Invalid date')) + + const response = await app.inject() + .post(adminTokenContract.createAdminToken.path) + .body({ + name: 'new-token', + expirationDate: null, + permissions: '4', + }) + .end() + + expect(businessCreateTokenMock).toHaveBeenCalledTimes(1) + expect(response.statusCode).toEqual(400) + }) + }) + + describe('deleteAdminToken', () => { + const tokenId = faker.string.uuid() + it('should delete a token for authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessDeleteTokenMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .delete(adminTokenContract.deleteAdminToken.path.replace(':tokenId', tokenId)) + .end() + + expect(businessDeleteTokenMock).toHaveBeenCalledWith(tokenId) + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(adminTokenContract.deleteAdminToken.path.replace(':tokenId', tokenId)) + .end() + + expect(businessDeleteTokenMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts new file mode 100644 index 000000000..c5a630e8d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts @@ -0,0 +1,44 @@ +import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared' +import { serverInstance } from '../../app.js' +import { createToken, deleteToken, listTokens } from './business.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@/utils/errors.js' + +export function adminTokenRouter() { + return serverInstance.router(adminTokenContract, { + listAdminTokens: async ({ request: req, query }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + const body = await listTokens(query) + + return { + status: 200, + body, + } + }, + + createAdminToken: async ({ request: req, body: data }) => { + const perms = await authUser(req) + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + const body = await createToken(data) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + deleteAdminToken: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + await deleteToken(params.tokenId) + + return { + status: 204, + body: null, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts new file mode 100644 index 000000000..7815132d8 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts @@ -0,0 +1,173 @@ +import { describe, expect, it, vi } from 'vitest' +import { faker } from '@faker-js/faker' +import type { Cluster, Environment } from '@prisma/client' +import prisma from '../../__mocks__/prisma.js' +import { hook } from '../../__mocks__/utils/hook-wrapper.ts' +import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '../../utils/errors.ts' +import { createCluster, deleteCluster, getClusterAssociatedEnvironments, getClusterDetails, getClusterUsage, listClusters, updateCluster } from './business.ts' + +vi.mock('../../utils/hook-wrapper.ts', async () => ({ + hook, +})) + +const userId = faker.string.uuid() +const requestId = faker.string.uuid() +const cluster: Cluster = { + id: faker.string.uuid(), + infos: faker.lorem.lines(2), + privacy: 'public', + createdAt: new Date(), + updatedAt: new Date(), + zoneId: faker.string.uuid(), + clusterResources: false, + kubeConfigId: faker.string.uuid(), + label: faker.string.alpha(10), + secretName: faker.string.alpha(10), + external: false, + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), +} +describe('test Cluster business logic', () => { + describe('listClusters', () => { + it('should filter for user', async () => { + prisma.cluster.findMany.mockResolvedValue([]) + await listClusters(userId) + expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1) + expect(prisma.cluster.findMany).toHaveBeenCalledWith({ select: expect.any(Object), where: { OR: [{ privacy: 'public' }, expect.any(Object), expect.any(Object), expect.any(Object)] } }) + }) + it('should not filter', async () => { + const dbStages = [{ id: faker.string.uuid() }] + prisma.cluster.findMany.mockResolvedValue([{ stages: dbStages }] as unknown as Cluster[]) + const response = await listClusters() + expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1) + expect(prisma.cluster.findMany).toHaveBeenCalledWith({ select: expect.any(Object), where: {} }) + expect(response[0].stageIds).toStrictEqual([dbStages[0].id]) + }) + }) + + describe('getClusterAssociatedEnvironments', () => { + it('should list all environments attached to a cluster', async () => { + const envName = faker.string.alpha(8) + const projectName = faker.string.alpha(8) + const ownerEmail = faker.internet.email() + const cpu = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) + const gpu = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) + const memory = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) + const envs = [{ name: envName, cpu, gpu, memory, project: { name: projectName, owner: { email: ownerEmail } } }] as unknown as Environment[] + prisma.environment.findMany.mockResolvedValue(envs) + const response = await getClusterAssociatedEnvironments(cluster.id) + expect(response).toStrictEqual([{ + name: envName, + project: projectName, + owner: ownerEmail, + cpu, + gpu, + memory, + }]) + }) + }) + + describe('getClusterDetails', () => { + it('should return a cluster details', async () => { + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) + await getClusterDetails(cluster.id) + }) + it('should return a cluster details, without infos in db', async () => { + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, infos: null, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) + const response = await getClusterDetails(cluster.id) + expect(response.infos).toBe('') + }) + }) + + describe('getClusterUsage', () => { + it('should return a cluster usage', async () => { + prisma.environment.aggregate.mockResolvedValue({ _count: {}, _avg: {}, _min: {}, _max: {}, _sum: { + cpu: 10, + gpu: 5, + memory: 20, + } }) + const response = await getClusterUsage(cluster.id) + expect(response).toStrictEqual({ + cpu: 10, + gpu: 5, + memory: 20, + }) + }) + }) + + describe('createCluster', () => { + it('should create cluster', async () => { + hook.cluster.upsert.mockResolvedValue({ failed: false }) + prisma.cluster.findUnique.mockResolvedValue(null) + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) + prisma.cluster.create.mockResolvedValue(cluster) + + const response = await createCluster({ + infos: faker.string.alpha(10), + zoneId: faker.string.uuid(), + privacy: 'public', + stageIds: [], + clusterResources: false, + kubeconfig: { cluster: { tlsServerName: faker.internet.domainName() }, user: {} }, + label: faker.string.alpha(10), + external: false, + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + }, userId, requestId) + + expect(response).not.instanceOf(ErrorResType) + expect(prisma.cluster.create).toHaveBeenCalled() + }) + }) + + describe('updateCluster', () => { + it('should update cluster', async () => { + hook.cluster.upsert.mockResolvedValue({ failed: false }) + prisma.cluster.findUnique.mockResolvedValue(cluster) + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) + prisma.cluster.update.mockResolvedValue(cluster) + + const response = await updateCluster({ + infos: faker.string.alpha(10), + zoneId: faker.string.uuid(), + privacy: 'public', + stageIds: [], + }, cluster.id, userId, requestId) + + expect(response).not.instanceOf(ErrorResType) + expect(prisma.cluster.update).toHaveBeenCalled() + }) + it('should return 404', async () => { + prisma.cluster.findUnique.mockResolvedValue(null) + const response = await updateCluster({ infos: faker.string.alpha(10) }, cluster.id, userId, requestId) + expect(response).instanceOf(NotFound404) + }) + }) + + describe('deleteCluster', () => { + it('should delete cluster', async () => { + hook.cluster.delete.mockResolvedValue({}) + await deleteCluster({ clusterId: cluster.id, userId, requestId }) + + expect(prisma.cluster.delete).toHaveBeenCalledTimes(1) + }) + it('should return failed hook', async () => { + hook.cluster.delete.mockResolvedValue({ failed: true }) + const response = await deleteCluster({ clusterId: cluster.id, userId, requestId }) + + expect(response).instanceOf(Unprocessable422) + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) + }) + it('should not delete cluster, env attached', async () => { + prisma.environment.findFirst.mockResolvedValue({ id: faker.string.uuid() } as Environment) + const response = await deleteCluster({ clusterId: cluster.id, userId, requestId }) + + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) + expect(response).instanceOf(BadRequest400) + }) + }) +}) + +// findUniqueOrThrow diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts new file mode 100644 index 000000000..cff0b1882 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts @@ -0,0 +1,230 @@ +import type { Prisma, Project, User } from '@prisma/client' +import type { Cluster, ClusterDetails, Kubeconfig, clusterContract } from '@cpn-console/shared' +import { ClusterDetailsSchema, ClusterPrivacy } from '@cpn-console/shared' +import { + addLogs, + createCluster as createClusterQuery, + deleteCluster as deleteClusterQuery, + getClusterById, + getClusterByLabel, + getClusterDetails as getClusterDetailsQuery, + getClusterEnvironments, + getProjectsByClusterId, + linkClusterToProjects, + linkZoneToClusters, + listClusters as listClustersQuery, + listStagesByClusterId, + removeClusterFromProject, + removeClusterFromStage, + updateCluster as updateClusterQuery, +} from '@/resources/queries-index.js' +import { linkClusterToStages } from '@/resources/stage/business.js' +import { validateSchema } from '@/utils/business.js' +import { hook } from '@/utils/hook-wrapper.js' +import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '@/utils/errors.js' +import prisma from '@/prisma.js' +import type { Resources } from '@/types/index.js' + +export async function listClusters(userId?: User['id']) { + const where: Prisma.ClusterWhereInput = userId + ? { + OR: [ + // Sélectionne tous les clusters publics + { privacy: 'public' }, + // Sélectionne les clusters associés aux projets dont l'user est membre + { + projects: { some: { members: { some: { userId } } } }, + }, + // Sélectionne les clusters associés aux projets dont l'user est owner + { + projects: { some: { ownerId: userId } }, + }, + // Sélectionne les clusters associés aux environnments appartenant à des projets dont l'user est membre + { + environments: { some: { project: { members: { some: { userId } } } } }, + }, + ], + } + : {} + const clusters = await listClustersQuery(where) + return clusters.map(({ stages, ...cluster }) => ({ + ...cluster, + stageIds: stages.map(({ id }) => id), + })) +} + +export async function getClusterAssociatedEnvironments(clusterId: string) { + const clusterEnvironments = await getClusterEnvironments(clusterId) + + return clusterEnvironments.map((environment) => { + return ({ + project: environment.project?.name, + name: environment.name, + owner: environment.project.owner.email, + cpu: environment.cpu, + gpu: environment.gpu, + memory: environment.memory, + }) + }) +} + +export async function getClusterDetails(clusterId: string): Promise { + const { infos, projects, stages, kubeconfig, ...details } = await getClusterDetailsQuery(clusterId) + return { + ...details, + infos: infos ?? '', + projectIds: projects.map(project => project.id), + stageIds: stages.map(({ id }) => id), + kubeconfig: { + cluster: kubeconfig.cluster as unknown as Kubeconfig['cluster'], + user: kubeconfig.user as unknown as Kubeconfig['user'], + }, + } +} + +export async function getClusterUsage(clusterId: string): Promise { + const clusterUsage = await prisma.environment.aggregate({ + _sum: { + memory: true, + cpu: true, + gpu: true, + }, + where: { + clusterId, + }, + }) + return { + cpu: clusterUsage._sum.cpu ?? 0, + gpu: clusterUsage._sum.gpu ?? 0, + memory: clusterUsage._sum.memory ?? 0, + } +} + +export async function createCluster(data: typeof clusterContract.createCluster.body._type, userId: User['id'], requestId: string) { + const isLabelTaken = await getClusterByLabel(data.label) + if (isLabelTaken) return new BadRequest400('Ce label existe déjà pour un autre cluster') + + data.projectIds = data.privacy === ClusterPrivacy.PUBLIC + ? [] + : data.projectIds ?? [] + + const { + projectIds, + stageIds, + kubeconfig, + zoneId, + ...clusterData + } = data + + const clusterCreated = await createClusterQuery(clusterData, kubeconfig, zoneId) + + if (data.privacy === ClusterPrivacy.DEDICATED && projectIds.length) { + await linkClusterToProjects(clusterCreated.id, projectIds) + } + + if (stageIds?.length) { + await linkClusterToStages(clusterCreated.id, stageIds) + } + + const hookReply = await hook.cluster.upsert(clusterCreated.id, zoneId) + await addLogs({ action: 'Create Cluster', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la création du cluster') + } + + return getClusterDetails(clusterCreated.id) +} + +export async function updateCluster(data: typeof clusterContract.updateCluster.body._type, clusterId: Cluster['id'], userId: User['id'], requestId: string): Promise { + if (data?.privacy === ClusterPrivacy.PUBLIC) delete data.projectIds + + const schemaValidation = ClusterDetailsSchema.partial().safeParse({ ...data, id: clusterId }) + const validateResult = validateSchema(schemaValidation) + if (validateResult instanceof ErrorResType) return validateResult + + const dbCluster = await getClusterById(clusterId) + if (!dbCluster) return new NotFound404() + + const { + projectIds, + stageIds, + kubeconfig, + zoneId, + ...clusterData + } = data + + const clusterUpdated = await updateClusterQuery(clusterId, clusterData, + // @ts-ignore + kubeconfig) + + // zone + if (zoneId) { + await linkZoneToClusters(zoneId, [clusterId]) + } + + // projects + const dbProjects = await getProjectsByClusterId(clusterId) + + let projectsToRemove: Project['id'][] = [] + + if (projectIds && clusterUpdated.privacy === ClusterPrivacy.DEDICATED) { + await linkClusterToProjects(clusterId, projectIds) + projectsToRemove = dbProjects?.map(project => project.id)?.filter(dbProjectId => !projectIds.includes(dbProjectId)) ?? [] + } else if (clusterUpdated.privacy === ClusterPrivacy.PUBLIC) { + projectsToRemove = dbProjects?.map(project => project.id) ?? [] + } + + for (const projectId of projectsToRemove) { + await removeClusterFromProject(clusterUpdated.id, projectId) + } + + // stages + if (stageIds) { + await linkClusterToStages(clusterId, stageIds) + + const dbStages = await listStagesByClusterId(clusterId) + if (dbStages) { + for (const stage of dbStages) { + if (!stageIds.includes(stage.id)) { + await removeClusterFromStage(clusterUpdated.id, stage.id) + } + } + } + } + + const hookReply = await hook.cluster.upsert(clusterId, dbCluster.zoneId) + await addLogs({ action: 'Update Cluster', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la mise à jour du cluster') + } + + return getClusterDetails(clusterId) +} + +interface DeleteClusterArgs { + clusterId: Cluster['id'] + userId?: User['id'] + requestId: string + force?: boolean +} +export async function deleteCluster({ clusterId, requestId, force, userId }: DeleteClusterArgs) { + let message: string | null = null + if (force) { + const envs = await prisma.environment.deleteMany({ + where: { clusterId }, + }) + message = `${envs.count} environnements supprimés de force, n'oubliez pas de reprovisionner les projets concernés` + } else { + const environment = await prisma.environment.findFirst({ where: { clusterId } }) + if (environment) return new BadRequest400('Impossible de supprimer le cluster, des environnements en activité y sont déployés') + } + + const hookReply = await hook.cluster.delete(clusterId) + await addLogs({ action: 'Delete Cluster', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la suppression du cluster') + } + + await deleteClusterQuery(clusterId) + return message +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts new file mode 100644 index 000000000..f11ccb33e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts @@ -0,0 +1,312 @@ +import type { Cluster, Environment, Kubeconfig, Prisma, Project, Stage } from '@prisma/client' +import prisma from '@/prisma.js' + +export async function getClustersAssociatedWithProject(projectId: Project['id']) { + const [ + clusterIdsHistory, + clusterIdsEnv, + ] = await Promise.all([ + prisma.projectClusterHistory.findMany({ + select: { + clusterId: true, + }, + where: { + projectId, + }, + }).then(history => history.map(({ clusterId }) => clusterId)), + prisma.cluster.findMany({ + where: { environments: { some: { project: { id: projectId } } } }, + select: { id: true }, + }).then(cluster => cluster.map(({ id }) => id)), + ]) + const clusterIds = [ + ...clusterIdsHistory, + ...clusterIdsEnv.filter(id => !clusterIdsHistory.includes(id)), + ] + return prisma.cluster.findMany({ + where: { id: { in: clusterIds } }, + select: { + id: true, + infos: true, + label: true, + external: true, + privacy: true, + secretName: true, + kubeconfig: true, + clusterResources: true, + cpu: true, + gpu: true, + memory: true, + zone: { + select: { + id: true, + slug: true, + argocdUrl: true, + label: true, + }, + }, + }, + }) +} + +export async function updateProjectClusterHistory(projectId: Project['id'], clusterIds: Cluster['id'][]) { + return prisma.$transaction([ + prisma.projectClusterHistory.deleteMany({ + where: { + AND: { + projectId, + clusterId: { notIn: clusterIds }, + }, + }, + }), + prisma.projectClusterHistory.createMany({ + data: clusterIds.map(clusterId => ({ clusterId, projectId })), + skipDuplicates: true, + }), + ]) +} + +export function getClusterById(id: Cluster['id']) { + return prisma.cluster.findUnique({ + where: { id }, + include: { kubeconfig: true }, + }) +} + +export function getClusterByIdOrThrow(id: Cluster['id']) { + return prisma.cluster.findUniqueOrThrow({ + where: { id }, + include: { kubeconfig: true, zone: true }, + }) +} + +export function getClusterEnvironments(clusterId: Cluster['id']) { + return prisma.environment.findMany({ + where: { clusterId }, + select: { + name: true, + cpu: true, + gpu: true, + memory: true, + project: { + select: { + slug: true, + name: true, + owner: true, + members: true, + }, + }, + }, + }) +} + +export function getClusterDetails(id: Cluster['id']) { + return prisma.cluster.findUniqueOrThrow({ + where: { id }, + select: { + createdAt: true, + projects: { + select: { + id: true, + }, + }, + id: true, + clusterResources: true, + infos: true, + external: true, + label: true, + privacy: true, + kubeconfig: true, + stages: true, + updatedAt: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, + }, + }) +} + +export function getClustersByIds(clusterIds: Cluster['id'][]) { + return prisma.cluster.findMany({ + where: { + id: { in: clusterIds }, + }, + include: { kubeconfig: true }, + }) +} + +export function getPublicClusters() { + return prisma.cluster.findMany({ + where: { privacy: 'public' }, + include: { zone: true }, + }) +} + +export async function getClusterNamesByZoneId(zoneId: string) { + const clusterNames = await prisma.cluster.findMany({ + where: { zoneId }, + select: { + label: true, + }, + }) + return clusterNames.map(({ label }) => label) +} + +export function getClusterByLabel(label: Cluster['label']) { + return prisma.cluster.findUnique({ where: { label } }) +} + +export function getClusterByEnvironmentId(id: Environment['id']) { + return prisma.cluster.findMany({ + where: { + environments: { + some: { id }, + }, + }, + include: { kubeconfig: true }, + }) +} + +export function getClustersWithProjectIdAndConfig() { + return prisma.cluster.findMany({ + select: { + id: true, + stages: true, + projects: { + where: { + status: { not: 'archived' }, + }, + select: { + id: true, + name: true, + slug: true, + status: true, + }, + }, + clusterResources: true, + label: true, + infos: true, + privacy: true, + secretName: true, + kubeconfig: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, + }, + }) +} + +export function listClusters(where: Prisma.ClusterWhereInput) { + return prisma.cluster.findMany({ + where, + select: { + id: true, + label: true, + stages: true, + clusterResources: true, + privacy: true, + infos: true, + external: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, + }, + }) +} + +export async function getProjectsByClusterId(id: Cluster['id']) { + return (await prisma.cluster.findUniqueOrThrow({ + where: { id }, + select: { projects: true }, + }))?.projects +} + +export async function listStagesByClusterId(id: Cluster['id']) { + return (await prisma.cluster.findUniqueOrThrow({ + where: { id }, + select: { stages: true }, + }))?.stages +} + +export function createCluster(data: Omit, kubeconfig: Pick, zoneId: string) { + return prisma.cluster.create({ + data: { + ...data, + // @ts-ignore + kubeconfig: { create: kubeconfig }, + zone: { + connect: { id: zoneId }, + }, + }, + }) +} + +export function updateCluster(id: Cluster['id'], data: Partial>, kubeconfig: Pick) { + return prisma.cluster.update({ + where: { id }, + data: { + ...data, + kubeconfig: { + // @ts-ignore + update: kubeconfig, + }, + }, + }) +} + +export function linkClusterToProjects(id: Cluster['id'], projectIds: Project['id'][]) { + return prisma.cluster.update({ + where: { id }, + data: { + projects: { + connect: projectIds.map(projectId => ({ id: projectId })), + }, + }, + }) +} + +export function linkClusterToStages(id: Cluster['id'], stageIds: Stage['id'][]) { + return prisma.cluster.update({ + where: { id }, + data: { + stages: { + connect: stageIds.map(stageId => ({ id: stageId })), + }, + }, + }) +} + +export function removeClusterFromProject(id: Cluster['id'], projectId: Project['id']) { + return prisma.cluster.update({ + where: { id }, + data: { + projects: { + disconnect: { + id: projectId, + }, + }, + }, + }) +} + +export function removeClusterFromStage(id: Cluster['id'], stageId: Stage['id']) { + return prisma.cluster.update({ + where: { id }, + data: { + stages: { + disconnect: { + id: stageId, + }, + }, + }, + }) +} + +export function deleteCluster(id: Cluster['id']) { + return prisma.cluster.delete({ + where: { id }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts new file mode 100644 index 000000000..b133835e3 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts @@ -0,0 +1,311 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ClusterDetails, Environment } from '@cpn-console/shared' +import { clusterContract } from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getUserMockInfos } from '../../utils/mocks.js' +import { BadRequest400 } from '../../utils/errors.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMock = vi.spyOn(business, 'listClusters') +const businessGetDetailsMock = vi.spyOn(business, 'getClusterDetails') +const businessGetUsageMock = vi.spyOn(business, 'getClusterUsage') +const businessGetEnvironmentsMock = vi.spyOn(business, 'getClusterAssociatedEnvironments') +const businessCreateMock = vi.spyOn(business, 'createCluster') +const businessUpdateMock = vi.spyOn(business, 'updateCluster') +const businessDeleteMock = vi.spyOn(business, 'deleteCluster') + +describe('test clusterContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + describe('listClusters', () => { + it('as non admin', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + businessListMock.mockResolvedValueOnce([]) + const response = await app.inject() + .get(clusterContract.listClusters.path) + .end() + + expect(businessListMock).toHaveBeenCalledWith(user.user.id) + + expect(response.json()).toStrictEqual([]) + expect(response.statusCode).toEqual(200) + }) + it('as admin', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + + businessListMock.mockResolvedValueOnce([]) + const response = await app.inject() + .get(clusterContract.listClusters.path) + .end() + + expect(businessListMock).toHaveBeenCalledWith() + + expect(response.json()).toStrictEqual([]) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('getClusterDetails', () => { + it('should return cluster details', async () => { + const cluster: ClusterDetails = { + id: faker.string.uuid(), + clusterResources: true, + infos: '', + external: false, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + } + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetDetailsMock.mockResolvedValueOnce(cluster) + const response = await app.inject() + .get(clusterContract.getClusterDetails.path.replace(':clusterId', cluster.id)) + .end() + + expect(businessGetDetailsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(cluster) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(clusterContract.getClusterDetails.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetDetailsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getClusterUsage', () => { + it('should return cluster usage', async () => { + const resources = { + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + } + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetUsageMock.mockResolvedValueOnce(resources) + const response = await app.inject() + .get(clusterContract.getClusterUsage.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetUsageMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(resources) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(clusterContract.getClusterUsage.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetUsageMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getClusterEnvironments', () => { + it('should return cluster environments', async () => { + const envs: Environment[] = [] + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetEnvironmentsMock.mockResolvedValueOnce(envs) + const response = await app.inject() + .get(clusterContract.getClusterEnvironments.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(clusterContract.getClusterEnvironments.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('createCluster', () => { + const cluster: ClusterDetails = { + id: faker.string.uuid(), + clusterResources: true, + infos: '', + external: true, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + } + + it('should return created cluster', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(cluster) + const response = await app.inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end() + + expect(response.json()).toEqual(cluster) + expect(response.statusCode).toEqual(201) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('updateCluster', () => { + const clusterId = faker.string.uuid() + const cluster: Omit = { + clusterResources: true, + infos: '', + external: false, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + } + + it('should return created cluster', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: clusterId, ...cluster }) + const response = await app.inject() + .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) + .body(cluster) + .end() + + expect(response.json()).toEqual({ id: clusterId, ...cluster }) + expect(response.statusCode).toEqual(200) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) + .body(cluster) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) + .body(cluster) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('deleteCluster', () => { + it('should return empty when delete', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts new file mode 100644 index 000000000..ffa5de8c4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts @@ -0,0 +1,125 @@ +import type { AsyncReturnType } from '@cpn-console/shared' +import { AdminAuthorized, clusterContract } from '@cpn-console/shared' +import { + createCluster, + deleteCluster, + getClusterAssociatedEnvironments, + getClusterDetails as getClusterDetailsBusiness, + getClusterUsage, + listClusters, + updateCluster, +} from './business.js' +import '@/types/index.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@/utils/errors.js' + +export function clusterRouter() { + return serverInstance.router(clusterContract, { + listClusters: async ({ request: req }) => { + const { adminPermissions, user } = await authUser(req) + + let body: AsyncReturnType = [] + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listClusters() + } else if (user) { + body = await listClusters(user.id) + } + + return { + status: 200, + body, + } + }, + + getClusterDetails: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const clusterId = params.clusterId + const cluster = await getClusterDetailsBusiness(clusterId) + + return { + status: 200, + body: cluster, + } + }, + + getClusterUsage: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const clusterId = params.clusterId + const usage = await getClusterUsage(clusterId) + + return { + status: 200, + body: usage, + } + }, + + createCluster: async ({ request: req, body: data }) => { + const { adminPermissions, user } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + + if (!user) return new Unauthorized401('Require to be requested from user not api key') + const body = await createCluster(data, user.id, req.id) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + getClusterEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const clusterId = params.clusterId + const environments = await getClusterAssociatedEnvironments(clusterId) + + return { + status: 200, + body: environments, + } + }, + + updateCluster: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user) return new Unauthorized401('Require to be requested from user not api key') + + const clusterId = params.clusterId + const body = await updateCluster(data, clusterId, user.id, req.id) + + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + deleteCluster: async ({ request: req, params, query: { force } }) => { + const { user, adminPermissions, tokenId } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user?.id && !tokenId) return new Unauthorized401('Your identity has not been found') + + const clusterId = params.clusterId + const body = await deleteCluster({ + clusterId, + userId: user?.id, + requestId: req.id, + force, + }) + + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts new file mode 100644 index 000000000..b1185efb7 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts @@ -0,0 +1,353 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Cluster, Environment, Project, ProjectMembers, ProjectRole, Stage, User } from '@prisma/client' +import prisma from '../../__mocks__/prisma.js' +import { hook } from '../../__mocks__/utils/hook-wrapper.ts' +import { checkClusterResources, checkProjectResources, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business.ts' +import { Result } from '../../utils/business.js' + +vi.mock('../../utils/hook-wrapper.ts', async () => ({ + hook, +})) + +const user: User = { + id: faker.string.uuid(), + createdAt: new Date(), + updatedAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + adminRoleIds: [], + type: 'human', + lastLogin: null, +} +const project: Project & { + clusters: Pick[] + members: ProjectMembers[] + roles: ProjectRole[] + owner: User +} = { + createdAt: new Date(), + updatedAt: new Date(), + description: '', + everyonePerms: 649n, + id: faker.string.uuid(), + locked: false, + name: faker.string.alphanumeric(8), + status: 'created', + ownerId: faker.string.uuid(), + owner: user, + limitless: false, + hprodCpu: faker.number.int({ min: 0, max: 1000 }), + hprodGpu: faker.number.int({ min: 0, max: 1000 }), + hprodMemory: faker.number.int({ min: 0, max: 1000 }), + prodCpu: faker.number.int({ min: 0, max: 1000 }), + prodGpu: faker.number.int({ min: 0, max: 1000 }), + prodMemory: faker.number.int({ min: 0, max: 1000 }), + clusters: [], + roles: [], + members: [], + slug: faker.string.alphanumeric(8), + lastSuccessProvisionningVersion: faker.string.numeric(), +} + +describe('test environment business', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('getProjectEnvironments', () => { + it('should query environment for projectId', async () => { + prisma.environment.findMany.mockResolvedValue([]) + const projectId = faker.string.uuid() + await getProjectEnvironments(projectId) + + expect(prisma.environment.findMany).toHaveBeenCalledTimes(1) + }) + }) + + describe('createEnvironment', () => { + const clusterId = faker.string.uuid() + const stageId = faker.string.uuid() + const env = { name: 'new-env' } + it('should create environment and trigger hook', async () => { + const requestId = faker.string.uuid() + const stageId = faker.string.uuid() + + prisma.environment.create.mockResolvedValue({ clusterId } as Environment) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const result = await createEnvironment({ + userId: user.id, + projectId: project.id, + name: env.name, + cpu: 0.1, + gpu: 0.5, + memory: 2.0, + clusterId, + stageId, + requestId, + }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + + it('should create environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid() + + prisma.environment.create.mockResolvedValue({ clusterId } as Environment) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const result = await createEnvironment({ + userId: user.id, + projectId: project.id, + name: env.name, + cpu: 0.1, + gpu: 0.5, + memory: 2.0, + clusterId, + stageId, + requestId, + }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + }) + }) + + describe('updateEnvironment', () => { + it('should update environment and trigger hook', async () => { + const requestId = faker.string.uuid() + const environmentId = faker.string.uuid() + + prisma.environment.update.mockResolvedValue({ projectId: project.id } as Environment) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const result = await updateEnvironment({ + user, + environmentId, + requestId, + cpu: 2.0, + gpu: 4.0, + memory: 12.5, + }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.update).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + + it('should update environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid() + const environmentId = faker.string.uuid() + + prisma.environment.update.mockResolvedValue({ projectId: project.id } as Environment) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const result = await updateEnvironment({ + user, + environmentId, + requestId, + cpu: 2.0, + gpu: 4.0, + memory: 12.5, + }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.update).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + }) + }) + + describe('deleteEnvironment', () => { + it('should delete environment and trigger hook', async () => { + const requestId = faker.string.uuid() + const environmentId = faker.string.uuid() + + prisma.environment.delete.mockResolvedValue({ projectId: project.id } as Environment) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const result = await deleteEnvironment({ environmentId, userId: user.id, projectId: project.id, requestId }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.delete).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + + it('should delete environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid() + const environmentId = faker.string.uuid() + + prisma.environment.delete.mockResolvedValue({ projectId: project.id } as Environment) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const result = await deleteEnvironment({ environmentId, userId: user.id, projectId: project.id, requestId }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.delete).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + }) + }) + + describe('checkClusterResources', () => { + it('should authorize cluster not yet configured', async () => { + const cluster: Cluster = { + cpu: 0, + gpu: 0, + memory: 0, + } as Cluster + const result = await checkClusterResources({ cpu: 1, gpu: 0, memory: 1 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + it('should authorize cluster not yet used', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 0, + gpu: 0, + memory: 0, + }, + } as any) + const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 7 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + it('should authorize cluster used but not full', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 2, + gpu: 0, + memory: 2, + }, + } as any) + const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 6 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + it('should refuse cluster without enough space', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 5, + gpu: 0, + memory: 5, + }, + } as any) + const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 6 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + expect(result.error).toEqual('Le cluster ne dispose pas de suffisamment de ressources : CPU, Mémoire.') + }) + it('should refuse cluster without GPU', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 2, + gpu: 0, + memory: 2, + }, + } as any) + const result = await checkClusterResources({ cpu: 2, gpu: 1, memory: 2 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + expect(result.error).toEqual('Le cluster ne dispose pas de suffisamment de ressources : GPU.') + }) + }) + + describe('checkProjectResources', () => { + const prodStage: Stage = { + id: faker.string.uuid(), + name: 'prod', + } + const hprodStage: Stage = { + id: faker.string.uuid(), + name: 'hprod', + } + it('should authorize prod deployment for project with hprod resource but no prod resources', async () => { + const project: Project = { + hprodCpu: 10, + hprodGpu: 10, + hprodMemory: 10, + prodCpu: 0, + prodGpu: 0, + prodMemory: 0, + } as Project + prisma.stage.findUnique.mockResolvedValue(prodStage) + prisma.stage.findMany.mockResolvedValue([prodStage]) + const result = await checkProjectResources({ cpu: 1, gpu: 0, memory: 1, stageId: prodStage.id }, project) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + it('should refuse hprod deployment for project with hprod resource but no prod resources', async () => { + const project: Project = { + hprodCpu: 10, + hprodGpu: 10, + hprodMemory: 10, + prodCpu: 0, + prodGpu: 0, + prodMemory: 0, + } as Project + prisma.stage.findUnique.mockResolvedValue(hprodStage) + prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]) + prisma.environment.aggregate.mockResolvedValue({ + _sum: { cpu: 0, gpu: 0, memory: 0 }, + } as any) + const result = await checkProjectResources({ cpu: 20, gpu: 20, memory: 20, stageId: hprodStage.id }, project) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + expect(result.error).toEqual('Le projet ne dispose pas de suffisamment de ressources : CPU, GPU, Mémoire.') + }) + it('should refuse overloading hprod deployment', async () => { + const project: Project = { + hprodCpu: 20, + hprodGpu: 20, + hprodMemory: 20, + prodCpu: 10, + prodGpu: 10, + prodMemory: 10, + } as Project + prisma.stage.findUnique.mockResolvedValue(hprodStage) + prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]) + prisma.environment.aggregate.mockResolvedValue({ + _sum: { cpu: 15, gpu: 15, memory: 15 }, + } as any) + const result = await checkProjectResources({ cpu: 5, gpu: 6, memory: 5, stageId: hprodStage.id }, project) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + expect(result.error).toEqual('Le projet ne dispose pas de suffisamment de ressources : GPU.') + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts new file mode 100644 index 000000000..b7f5b5c34 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts @@ -0,0 +1,300 @@ +import type { Cluster, Environment, Project, Stage, User } from '@prisma/client' +import { + addLogs, + deleteEnvironment as deleteEnvironmentQuery, + getEnvironmentsByProjectId, + initializeEnvironment, + updateEnvironment as updateEnvironmentQuery, +} from '@/resources/queries-index.js' +import type { Resources, UserDetails } from '@/types/index.js' +import { hook } from '@/utils/hook-wrapper.js' +import prisma from '@/prisma.js' +import { Result } from '@/utils/business.js' + +export function getProjectEnvironments(projectId: Project['id']) { + return getEnvironmentsByProjectId(projectId) +} + +// Routes logic +interface CreateEnvironmentParam { + userId: User['id'] + projectId: Project['id'] + name: Environment['name'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] + clusterId: Environment['clusterId'] + stageId: Stage['id'] + requestId: string +} + +interface CreateEnvironmentResult { + id: Environment['id'] + createdAt: Date + updatedAt: Date + projectId: Project['id'] + name: Environment['name'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] + clusterId: Environment['clusterId'] + stageId: Stage['id'] +} + +export async function createEnvironment({ + userId, + projectId, + name, + cpu, + gpu, + memory, + clusterId, + stageId, + requestId, +}: CreateEnvironmentParam): Promise> { + const environment = await initializeEnvironment({ projectId, name, cpu, gpu, memory, clusterId, stageId }) + + const { results } = await hook.project.upsert(projectId) + await addLogs({ action: 'Create Environment', data: results, userId, requestId, projectId }) + if (results.failed) { + return Result.fail('Echec des services à la création de l\'environnement') + } + + return Result.succeed({ + ...environment, + stageId, + }) +} + +interface UpdateEnvironmentParam { + user: UserDetails + environmentId: Environment['id'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] + requestId: string +} + +export async function updateEnvironment({ + user, + environmentId, + requestId, + cpu, + gpu, + memory, +}: UpdateEnvironmentParam) { + const env = await updateEnvironmentQuery({ + id: environmentId, + cpu, + gpu, + memory, + }) + const { results } = await hook.project.upsert(env.projectId) + await addLogs({ action: 'Update Environment', data: results, userId: user.id, requestId, projectId: env.projectId }) + if (results.failed) { + return Result.fail('Echec des services à la mise à jour de l\'environnement') + } + + return Result.succeed(env) +} + +interface DeleteEnvironmentParam { + userId?: User['id'] + environmentId: Environment['id'] + projectId: Project['id'] + requestId: string +} + +export async function deleteEnvironment({ + userId, + environmentId, + projectId, + requestId, +}: DeleteEnvironmentParam) { + const env = await deleteEnvironmentQuery(environmentId) + + const { results } = await hook.project.upsert(projectId) + await addLogs({ action: 'Delete Environment', data: results, userId, requestId, projectId: env.projectId }) + if (results.failed) { + return Result.fail('Echec des services à la suppression de l\'environnement') + } + return Result.succeed(null) +} + +export async function checkEnvironmentCreate(input: { + clusterId: Cluster['id'] + projectId: Project['id'] + name: Environment['name'] + stageId: Stage['id'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] +}): Promise> { + const errorMessages: string[] = [] + const [stage, sameNameEnvironment, cluster] = await Promise.all([ + input.stageId + ? prisma.stage.findUnique({ where: { id: input.stageId } }) + : undefined, + input.name + ? prisma.environment.findUnique({ where: { projectId_name: { projectId: input.projectId, name: input.name } } }) + : undefined, + input.clusterId + ? prisma.cluster.findFirst({ + where: { + OR: [{ // un cluster public + id: input.clusterId, + privacy: 'public', + }, { + id: input.clusterId, // un cluster dédié rattaché au projet + privacy: 'dedicated', + projects: { some: { id: input.projectId } }, + }], + }, + }) + : undefined, + ]) + if (sameNameEnvironment) errorMessages.push('Ce nom d\'environnement est déjà pris.') + if (!stage) errorMessages.push('Type d\'environnment invalide.') + if (!cluster) { + errorMessages.push('Cluster invalide.') + } else { + const resourceCheckResult = await checkClusterResources(input, cluster) + if (resourceCheckResult.isError) { + errorMessages.push(resourceCheckResult.error) + } + const project = await prisma.project.findUniqueOrThrow({ where: { id: input.projectId } }) + const projectCheckResult = await checkProjectResources(input, project) + if (projectCheckResult.isError) { + errorMessages.push(projectCheckResult.error) + } + } + if (errorMessages.length > 0) { + return Result.fail(errorMessages.join('\n')) + } + return Result.succeed(true) +} + +export async function checkClusterResources(input: { + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] +}, cluster: Cluster): Promise> { + if (cluster.cpu === 0 && cluster.memory === 0) { + // Unconfigured cluster + return Result.succeed(true) + } + const unsufficientResource = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { cpu: cluster.cpu, gpu: cluster.gpu, memory: cluster.memory }, + where: { + cluster: { + id: cluster.id, + }, + }, + }) + if (unsufficientResource.length > 0) { + return Result.fail(`Le cluster ne dispose pas de suffisamment de ressources : ${unsufficientResource.join(', ')}.`) + } + return Result.succeed(true) +} + +export async function checkProjectResources(input: { + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] + stageId: Environment['stageId'] +}, project: Project): Promise> { + if (project.limitless) { + // No limits + return Result.succeed(true) + } + const stage = await prisma.stage.findUnique({ where: { id: input.stageId } }) + const prodStages = await prisma.stage.findMany({ select: { id: true }, where: { name: 'prod' } }) + let overflowResources: string[] + if (stage?.name === 'prod') { + overflowResources = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { cpu: project.prodCpu, gpu: project.prodGpu, memory: project.prodMemory }, + where: { + projectId: project.id, + stageId: { + in: prodStages.map(s => s.id), + }, + }, + }) + } else { // hprod + overflowResources = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { cpu: project.hprodCpu, gpu: project.hprodGpu, memory: project.hprodMemory }, + where: { + projectId: project.id, + stageId: { + notIn: prodStages.map(s => s.id), + }, + }, + }) + } + if (overflowResources.length > 0) { + return Result.fail(`Le projet ne dispose pas de suffisamment de ressources : ${overflowResources.join(', ')}.`) + } + return Result.succeed(true) +} + +export async function checkEnvironmentUpdate(input: { + environmentId: Environment['id'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] +}): Promise> { + const environment = await prisma.environment.findUniqueOrThrow({ + select: { cluster: true, projectId: true, stageId: true }, + where: { id: input.environmentId }, + }) + const cluster = await prisma.cluster.findUniqueOrThrow({ + where: { id: environment.cluster.id }, + }) + const errorMessages: string[] = [] + const resourceCheckResult = await checkClusterResources(input, cluster) + if (resourceCheckResult.isError) { + errorMessages.push(resourceCheckResult.error) + } + const project = await prisma.project.findUniqueOrThrow({ where: { id: environment.projectId } }) + const projectCheckResult = await checkProjectResources({ stageId: environment.stageId, ...input }, project) + if (projectCheckResult.isError) { + errorMessages.push(projectCheckResult.error) + } + if (errorMessages.length > 0) { + return Result.fail(errorMessages.join('\n')) + } + return Result.succeed(true) +} + +export async function getOverflowResources({ request, limit, where }: { + request: Resources + limit: Resources + where: any +}): Promise { + if (limit.cpu === 0 && limit.memory === 0) { + // Unconfigured project prod resources + return [] + } + const environmentResources = await prisma.environment.aggregate({ + _sum: { + memory: true, + cpu: true, + gpu: true, + }, + where, + }) + const unsufficientResource: string[] = [] + if ((environmentResources._sum.cpu ?? 0) + request.cpu > limit.cpu) { + unsufficientResource.push('CPU') + } + if ((environmentResources._sum.gpu ?? 0) + request.gpu > limit.gpu) { + unsufficientResource.push('GPU') + } + if ((environmentResources._sum.memory ?? 0) + request.memory > limit.memory) { + unsufficientResource.push('Mémoire') + } + return unsufficientResource +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts new file mode 100644 index 000000000..620e693cb --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts @@ -0,0 +1,98 @@ +import type { Environment, Prisma, Project } from '@prisma/client' +import prisma from '@/prisma.js' + +// SELECT +export function getEnvironmentByIdOrThrow(id: Environment['id']) { + return prisma.environment.findUniqueOrThrow({ where: { id }, include: { stage: true } }) +} + +export function getEnvironmentInfos(id: Environment['id']) { + return prisma.environment.findUniqueOrThrow({ + where: { id }, + include: { + project: { + select: { + owner: true, + name: true, + id: true, + status: true, + repositories: { + where: { isInfra: true }, + }, + locked: true, + clusters: { + select: { + id: true, + label: true, + privacy: true, + clusterResources: true, + }, + }, + }, + }, + stage: true, + }, + }) +} + +export async function getEnvironmentsByProjectId(projectId: Project['id']) { + return prisma.environment.findMany({ + where: { projectId }, + include: { + stage: true, + }, + }) +} + +export function getEnvironmentByIdWithCluster(id: Environment['id']) { + return prisma.environment.findUnique({ + where: { id }, + include: { + cluster: { + include: { kubeconfig: true }, + }, + }, + }) +} + +// INSERT +export function initializeEnvironment(data: Prisma.EnvironmentUncheckedCreateInput) { + return prisma.environment.create({ + data, + include: { + project: { + include: { + repositories: { + where: { isInfra: true }, + }, + }, + }, + }, + }) +} + +export function updateEnvironment({ id, cpu, gpu, memory }: { id: Environment['id'], cpu: Environment['cpu'], gpu: Environment['gpu'], memory: Environment['memory'] }) { + return prisma.environment.update({ + where: { + id, + }, + data: { + cpu, + gpu, + memory, + }, + }) +} + +// DELETE +export function deleteEnvironment(id: Environment['id']) { + return prisma.environment.delete({ + where: { id }, + }) +} + +export function deleteAllEnvironmentForProject(id: Project['id']) { + return prisma.environment.deleteMany({ + where: { projectId: id }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts new file mode 100644 index 000000000..47337bbb4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts @@ -0,0 +1,372 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { type Environment, PROJECT_PERMS, environmentContract } from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { atDates, getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetProjectEnvironmentsMock = vi.spyOn(business, 'getProjectEnvironments') +const businessCreateEnvironmentMock = vi.spyOn(business, 'createEnvironment') +const businessUpdateEnvironmentMock = vi.spyOn(business, 'updateEnvironment') +const businessDeleteEnvironmentMock = vi.spyOn(business, 'deleteEnvironment') +const businessCheckEnvironmentCreateMock = vi.spyOn(business, 'checkEnvironmentCreate') +const businessCheckEnvironmentUpdateMock = vi.spyOn(business, 'checkEnvironmentUpdate') + +describe('environmentRouter tests', () => { + let projectId: string + let environmentId: string + let environmentData: Omit + + beforeEach(() => { + vi.resetAllMocks() + projectId = faker.string.uuid() + environmentId = faker.string.uuid() + environmentData = { + projectId, + name: 'envname', + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + clusterId: faker.string.uuid(), + stageId: faker.string.uuid(), + } + }) + + describe('listEnvironments', () => { + it('should return environments for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetProjectEnvironmentsMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(environmentContract.listEnvironments.path) + .query({ projectId }) + .end() + + expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledWith(projectId) + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should return empty for non member of projectId query ', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(environmentContract.listEnvironments.path) + .query({ projectId }) + .end() + + expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledTimes(0) + expect(response.json()).toEqual([]) + }) + }) + + describe('createEnvironment', () => { + it('should create environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ success: true }) + businessCreateEnvironmentMock.mockResolvedValueOnce({ + success: true, + data: { id: environmentId, ...environmentData, ...atDates }, + }) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.json()).toMatchObject({ id: environmentId, ...environmentData }) + expect(response.statusCode).toEqual(201) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(403) + }) + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ success: true, message: 'pas d erreur' }) + businessCreateEnvironmentMock.mockResolvedValueOnce({ isError: true, message: 'une erreur' }) + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(500) + }) + it('should pass invalid reason error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ isError: true, message: 'une erreur' }) + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('updateEnvironment', () => { + let updateData: { cpu: number, gpu: number, memory: number } + beforeEach(() => { + updateData = { + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + } + }) + it('should update environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ success: true, value: true }) + businessUpdateEnvironmentMock.mockResolvedValueOnce({ success: true, data: { id: environmentId, ...environmentData, ...atDates } }) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.json()).toMatchObject({ id: environmentId, ...environmentData }) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 404 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(404) + }) + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateEnvironmentMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(500) + }) + it('should pass invalid reason error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('deleteEnvironment', () => { + it('should delete environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteEnvironmentMock.mockResolvedValueOnce({ success: true }) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(204) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteEnvironmentMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(500) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts new file mode 100644 index 000000000..f1cf21b8a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts @@ -0,0 +1,109 @@ +import { ProjectAuthorized, environmentContract } from '@cpn-console/shared' +import { checkEnvironmentCreate, checkEnvironmentUpdate, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401 } from '@/utils/errors.js' + +export function environmentRouter() { + return serverInstance.router(environmentContract, { + listEnvironments: async ({ request: req, query }) => { + const projectId = query.projectId + const perms = await authUser(req, { id: projectId }) + + const environments = ProjectAuthorized.ListEnvironments(perms) + ? await getProjectEnvironments(projectId) + : [] + + return { + status: 200, + body: environments, + } + }, + + createEnvironment: async ({ request: req, body: requestBody }) => { + const projectId = requestBody.projectId + const perms = await authUser(req, { id: projectId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const checkCreateResult = await checkEnvironmentCreate({ ...requestBody }) + if (checkCreateResult.isError) return new BadRequest400(checkCreateResult.error) + + const result = await createEnvironment({ + userId: perms.user.id, + projectId, + name: requestBody.name, + clusterId: requestBody.clusterId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + stageId: requestBody.stageId, + requestId: req.id, + }) + if (result.isError) { + return new Internal500(result.error) + } + return { + status: 201, + body: result.data, + } + }, + + updateEnvironment: async ({ request: req, body: requestBody, params }) => { + const { environmentId } = params + const perms = await authUser(req, { environmentId }) + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!ProjectAuthorized.ListEnvironments(perms)) return new NotFound404() + if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const checkUpdateResult = await checkEnvironmentUpdate({ environmentId, ...requestBody }) + if (checkUpdateResult.isError) return new BadRequest400(checkUpdateResult.error) + + const result = await updateEnvironment({ + user: perms.user, + environmentId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + requestId: req.id, + }) + if (result.isError) { + return new Internal500(result.error) + } + return { + status: 200, + body: result.data, + } + }, + + deleteEnvironment: async ({ request: req, params }) => { + const { environmentId } = params + const perms = await authUser(req, { environmentId }) + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const result = await deleteEnvironment({ + userId: perms.user?.id, + environmentId, + requestId: req.id, + projectId: perms.projectId, + }) + if (result.isError) { + return new Internal500(result.error) + } + + return { + status: 204, + body: result.data, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts new file mode 100644 index 000000000..5c0c48f4d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts @@ -0,0 +1,49 @@ +import type { FastifyInstance } from 'fastify' +import { serverInstance } from '@/app.js' + +import { adminRoleRouter } from './admin-role/router.js' +import { adminTokenRouter } from './admin-token/router.js' +import { clusterRouter } from './cluster/router.js' +import { environmentRouter } from './environment/router.js' +import { logRouter } from './log/router.js' +import { personalAccessTokenRouter } from './user/tokens/router.js' +import { pluginConfigRouter } from './system/config/router.js' +import { projectMemberRouter } from './project-member/router.js' +import { projectRoleRouter } from './project-role/router.js' +import { projectRouter } from './project/router.js' +import { projectServiceRouter } from './project-service/router.js' +import { repositoryRouter } from './repository/router.js' +import { serviceChainRouter } from './service-chain/router.js' +import { serviceMonitorRouter } from './service-monitor/router.js' +import { stageRouter } from './stage/router.js' +import { systemRouter } from './system/router.js' +import { systemSettingsRouter } from './system/settings/router.js' +import { userRouter } from './user/router.js' +import { zoneRouter } from './zone/router.js' + +// relax validation schema if NO_VALIDATION env var is set to true. +// /!\ It can lead to security leaks !!!! +const validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' } +export function apiRouter() { + return async (app: FastifyInstance) => { + await app.register(serverInstance.plugin(adminRoleRouter()), validateTrue) + await app.register(serverInstance.plugin(adminTokenRouter()), validateTrue) + await app.register(serverInstance.plugin(clusterRouter()), validateTrue) + await app.register(serverInstance.plugin(serviceChainRouter()), validateTrue) + await app.register(serverInstance.plugin(environmentRouter()), validateTrue) + await app.register(serverInstance.plugin(logRouter()), validateTrue) + await app.register(serverInstance.plugin(personalAccessTokenRouter()), validateTrue) + await app.register(serverInstance.plugin(projectRouter()), validateTrue) + await app.register(serverInstance.plugin(projectMemberRouter()), validateTrue) + await app.register(serverInstance.plugin(projectRoleRouter()), validateTrue) + await app.register(serverInstance.plugin(projectServiceRouter()), validateTrue) + await app.register(serverInstance.plugin(repositoryRouter()), validateTrue) + await app.register(serverInstance.plugin(serviceMonitorRouter()), validateTrue) + await app.register(serverInstance.plugin(pluginConfigRouter()), validateTrue) + await app.register(serverInstance.plugin(stageRouter()), validateTrue) + await app.register(serverInstance.plugin(systemRouter()), validateTrue) + await app.register(serverInstance.plugin(systemSettingsRouter()), validateTrue) + await app.register(serverInstance.plugin(userRouter()), validateTrue) + await app.register(serverInstance.plugin(zoneRouter()), validateTrue) + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts new file mode 100644 index 000000000..9abaedc63 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { faker } from '@faker-js/faker' +import prisma from '../../__mocks__/prisma.js' +import { getLogs } from './business.ts' + +describe('test log business', () => { + it('should map filter (clean logs)', async () => { + const dbLogs = [{ + data: { args: {} }, + createdAt: new Date(), + updatedAt: new Date(), + userId: null, + action: 'Action', + id: faker.string.uuid(), + }] + const query = { limit: 10, offset: 10, clean: true, projectId: undefined } + prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]) + const [_total, logs] = await getLogs(query) + + expect(logs[0]).not.haveOwnProperty('requestId') + expect(logs[0].data).not.haveOwnProperty('results') + expect(logs[0].data).not.haveOwnProperty('args') + expect(logs[0].data).not.haveOwnProperty('config') + }) + + it('should not filter (admin logs)', async () => { + const dbLogs = [{ + data: { args: {} }, + createdAt: new Date(), + updatedAt: new Date(), + userId: null, + action: 'Action', + id: faker.string.uuid(), + }] + const query = { limit: 10, offset: 10, clean: false, projectId: undefined } + prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]) + const [_total, logs] = await getLogs(query) + + expect(logs[0].data).haveOwnProperty('args') + expect(logs[0].data).not.haveOwnProperty('config') + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts new file mode 100644 index 000000000..9a0182e7e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts @@ -0,0 +1,13 @@ +import type { logContract } from '@cpn-console/shared' +import { CleanLogSchema } from '@cpn-console/shared' +import { getAllLogs } from '@/resources/queries-index.js' + +export async function getLogs({ offset, limit, projectId, clean }: typeof logContract.getLogs.query._type) { + const [total, logs] = await getAllLogs({ skip: offset, take: limit, where: { projectId } }) + return [ + total, + clean + ? logs.map(log => CleanLogSchema.parse(log)) + : logs, + ] +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts new file mode 100644 index 000000000..3851a8f13 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts @@ -0,0 +1,57 @@ +import type { Log, Prisma, Project, User } from '@prisma/client' +import { exclude } from '@cpn-console/shared' +import prisma from '@/prisma.js' + +// SELECT +export function getAllLogsForUser(user: User, offset = 0) { + return prisma.log.findMany({ + where: { userId: user.id }, + take: 100, + skip: offset, + }) +} + +export function getAllLogs({ skip = 0, take = 5, where }: Prisma.LogFindManyArgs) { + return prisma.$transaction([ + prisma.log.count({ where }), + prisma.log.findMany({ + orderBy: { + createdAt: 'desc', + }, + skip, + take, + where, + }), + ]) +} + +// CREATE +interface AddLogsArgs { + action: Log['action'] + data: Record + userId?: User['id'] | null + requestId: string + projectId?: Project['id'] +} +export function addLogs({ action, data, requestId, userId = null, projectId }: AddLogsArgs) { + return prisma.log.create({ + data: { + action, + userId, + data: exclude(data, ['cluster', 'user', 'newCreds', 'apis']), + requestId, + projectId, + }, + }) +} + +// TECH +export function _createLog(data: Parameters[0]['create']) { + return prisma.log.upsert({ + where: { + id: data.id, + }, + create: data, + update: data, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts new file mode 100644 index 000000000..d6c144f75 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { logContract } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetLogsMock = vi.spyOn(business, 'getLogs') + +describe('test logContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('getLogs', () => { + it('should return logs for admin', async () => { + const user = getUserMockInfos(true) + const logs = [] + const total = 1 + + authUserMock.mockResolvedValueOnce(user) + businessGetLogsMock.mockResolvedValueOnce([total, logs]) + + const response = await app.inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0 }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetLogsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ total, logs }) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for non-admin, no projectId', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0 }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetLogsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + + it('should return logs for non-admin, with projectId', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 1n }) + const user = getUserMockInfos(false, undefined, projectPerms) + const projectId = faker.string.uuid() + + const logs = [] + const total = 1 + + businessGetLogsMock.mockResolvedValueOnce([total, logs]) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0, projectId, clean: false }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetLogsMock).toHaveBeenCalledWith({ clean: true, limit: 10, offset: 0, projectId }) + expect(response.statusCode).toEqual(200) + }) + + it('should not return logs for non-admin, with projectId', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + const projectId = faker.string.uuid() + + const logs = [] + const total = 1 + + businessGetLogsMock.mockResolvedValueOnce([total, logs]) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0, projectId, clean: false }) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts new file mode 100644 index 000000000..e3e3247cf --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts @@ -0,0 +1,32 @@ +import type { CleanLog, Log, XOR } from '@cpn-console/shared' +import { AdminAuthorized, logContract } from '@cpn-console/shared' +import { getLogs } from './business.js' +import { serverInstance } from '@/app.js' +import type { UserProfile, UserProjectProfile } from '@/utils/controller.js' +import { authUser } from '@/utils/controller.js' +import { Forbidden403 } from '@/utils/errors.js' + +export function logRouter() { + return serverInstance.router(logContract, { + // Récupérer des logs + getLogs: async ({ request: req, query }) => { + const perms: XOR = query.projectId + ? await authUser(req, { id: query.projectId }) + : await authUser(req) + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { + if (!perms.projectPermissions) { + return new Forbidden403() + } + query.clean = true + } + + const [total, logs] = await getLogs(query) as [number, unknown[]] as [number, Array] + + return { + status: 200, + body: { total, logs }, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts new file mode 100644 index 000000000..392b7dd15 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts @@ -0,0 +1,60 @@ +import type { Project, User } from '@prisma/client' +import type { XOR, projectMemberContract } from '@cpn-console/shared' +import { UserSchema } from '@cpn-console/shared' +import { logViaSession } from '../user/business.js' +import { + addLogs, + deleteMember, + listMembers as listMembersQuery, + upsertMember, +} from '@/resources/queries-index.js' +import prisma from '@/prisma.js' +import { BadRequest400, NotFound404 } from '@/utils/errors.js' +import { hook } from '@/utils/hook-wrapper.js' + +export const listMembers = async (projectId: Project['id']) => listMembersQuery(projectId) + +export async function addMember(projectId: Project['id'], user: XOR<{ userId: string }, { email: string }>, requestorId: User['id'], requestId: string, projectOwnerId: Project['ownerId']) { + let userInDb: User | undefined | null + + if (user.userId) { + userInDb = await prisma.user.findUnique({ where: { id: user.userId, type: 'human' } }) + } else if (user.email) { + userInDb = await prisma.user.findUnique({ where: { email: user.email, type: 'human' } }) + } else { + return new BadRequest400('Veuillez spécifiez au moins un userId ou un email') + } + if (userInDb) { + if (userInDb.id === projectOwnerId) return new BadRequest400('Le owner ne peut pas être ajouté à cette liste') + } else if (user.email) { + const hookReply = await hook.user.retrieveUserByEmail(user.email) + await addLogs({ action: 'Retrieve User By Email', data: hookReply, userId: requestorId, requestId }) + if (hookReply.failed) { + throw new BadRequest400('Echec de la recherche auprès des services externes') + } + + const retrievedUser = hookReply.results.keycloak?.user + if (!retrievedUser) return new BadRequest400('Utilisateur introuvable') + const userValidated = UserSchema.pick({ email: true, firstName: true, lastName: true, id: true }).safeParse(retrievedUser) + if (!userValidated.success) return new BadRequest400('L\'utilisateur trouvé ne remplit pas les conditions de vérification') + const logResults = await logViaSession({ ...userValidated.data, groups: [] }) + userInDb = logResults.user + } else { + return new NotFound404() + } + + await upsertMember({ projectId, userId: userInDb.id, roleIds: [] }) + return listMembers(projectId) +} + +export async function patchMembers(projectId: Project['id'], members: typeof projectMemberContract.patchMembers.body._type) { + for (const member of members) { + await upsertMember({ projectId, userId: member.userId, roleIds: member.roles }) + } + return listMembers(projectId) +} + +export async function removeMember(projectId: Project['id'], userId: User['id']) { + await deleteMember({ projectId, userId }) + return listMembers(projectId) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts new file mode 100644 index 000000000..a4ceb00df --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts @@ -0,0 +1,33 @@ +import type { + Prisma, + + Project, +} from '@prisma/client' + +import prisma from '@/prisma.js' + +export const listMembers = (projectId: Project['id']) => prisma.projectMembers.findMany({ where: { projectId }, include: { user: true } }) + +export function upsertMember(data: Prisma.ProjectMembersUncheckedCreateInput) { + return prisma.projectMembers.upsert({ + where: { + projectId_userId: { + userId: data.userId, + projectId: data.projectId, + }, + }, + create: data, + update: { + roleIds: data.roleIds, + }, + include: { user: true }, + }) +} + +export function deleteMember(data: Prisma.ProjectMembersWhereUniqueInput['projectId_userId']) { + return prisma.projectMembers.delete({ + where: { + projectId_userId: data, + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts new file mode 100644 index 000000000..2cb64f749 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts @@ -0,0 +1,294 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Member } from '@cpn-console/shared' +import { PROJECT_PERMS, projectMemberContract } from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' +import { BadRequest400 } from '../../utils/errors.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMembersMock = vi.spyOn(business, 'listMembers') +const businessAddMemberMock = vi.spyOn(business, 'addMember') +const businessPatchMembersMock = vi.spyOn(business, 'patchMembers') +const businessRemoveMemberMock = vi.spyOn(business, 'removeMember') + +describe('projectMemberRouter tests', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + const projectId = faker.string.uuid() + const userId = faker.string.uuid() + + describe('listMembers', () => { + it('should return members for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessListMembersMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectMemberContract.listMembers.path.replace(':projectId', projectId)) + .end() + + expect(businessListMembersMock).toHaveBeenCalledWith(projectId) + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectMemberContract.listMembers.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + }) + + describe('addMember', () => { + const memberData: Partial = { + userId: faker.string.uuid(), + } + + it('should add member for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + const newMember = { + ...memberData, + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + roleIds: [], + } + + businessAddMemberMock.mockResolvedValueOnce([newMember]) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.json()).toEqual([newMember]) + expect(response.statusCode).toEqual(201) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + businessAddMemberMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) + + describe('patchMembers', () => { + const patchData = [{ userId: faker.string.uuid(), roles: [] }] + + it('should patch members for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessPatchMembersMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) + + describe('removeMember', () => { + it('should remove member for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessRemoveMemberMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should be able leave a project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessRemoveMemberMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts new file mode 100644 index 000000000..6b0e3f80e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts @@ -0,0 +1,82 @@ +import { AdminAuthorized, ProjectAuthorized, projectMemberContract } from '@cpn-console/shared' +import { + addMember, + listMembers, + patchMembers, + removeMember, +} from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@/utils/errors.js' + +export function projectMemberRouter() { + return serverInstance.router(projectMemberContract, { + listMembers: async ({ request: req, params }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + const body = await listMembers(projectId) + + return { + status: 200, + body, + } + }, + + addMember: async ({ request: req, params, body }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await addMember(projectId, body, perms.user.id, req.id, perms.projectOwnerId) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 201, + body: resBody, + } + }, + + patchMembers: async ({ request: req, params, body }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await patchMembers(projectId, body) + + return { + status: 200, + body: resBody, + } + }, + + removeMember: async ({ request: req, params }) => { + const { projectId, userId } = params + const perms = await authUser(req, { id: projectId }) + + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + if (!ProjectAuthorized.ManageMembers(perms) && userId !== perms.user?.id) return new Forbidden403() + + const resBody = await removeMember(projectId, params.userId) + + return { + status: 200, + body: resBody, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts new file mode 100644 index 000000000..cdbaa3fd1 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts @@ -0,0 +1,195 @@ +import { faker } from '@faker-js/faker' +import { describe, expect, it } from 'vitest' +import type { ProjectMembers, ProjectRole, User } from '@prisma/client' +import prisma from '../../__mocks__/prisma.js' +import { BadRequest400 } from '../../utils/errors.ts' +import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business.ts' + +const projectId = faker.string.uuid() +describe('test project-role business', () => { + describe('listRoles', () => { + it('should stringify bigint', async () => { + const partialRole: Partial = { + permissions: 4n, + } + + prisma.projectRole.findMany.mockResolvedValueOnce([partialRole]) + const response = await listRoles(projectId) + expect(response).toEqual([{ permissions: '4' }]) + }) + }) + + describe('createRole', () => { + it('should create role with incremented position when position 0 is the highest', async () => { + const dbRole: Partial = { + projectId, + permissions: 4n, + position: 0, + } + + prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole) + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.projectRole.create.mockResolvedValue(null) + await createRole(projectId, { name: 'test', permissions: '4' }) + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 1, projectId } }) + }) + + it('should create role with incremented position with bigger position', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + } + + prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole) + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.projectRole.create.mockResolvedValue(null) + await createRole(projectId, { name: 'test', permissions: '4' }) + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 51, projectId } }) + }) + + it('should create role with incremented position with no role in db', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + } + + prisma.projectRole.findFirst.mockResolvedValueOnce(undefined) + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.projectRole.create.mockResolvedValue(null) + await createRole(projectId, { name: 'test', permissions: '4' }) + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 0, projectId } }) + }) + }) + + describe('deleteRole', () => { + const roleId = faker.string.uuid() + it('should delete role and remove id from concerned users', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + id: faker.string.uuid(), + } + const members = [{ + userId: faker.string.uuid(), + projectId, + roleIds: [roleId], + }, { + userId: faker.string.uuid(), + projectId, + roleIds: [roleId, faker.string.uuid()], + }] as const satisfies Partial[] + + prisma.projectMembers.findMany.mockResolvedValueOnce(members) + prisma.projectRole.findMany.mockResolvedValueOnce([]) + prisma.projectRole.delete.mockResolvedValue(dbRole) + await deleteRole(roleId) + + expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(1, { where: expect.any(Object), data: { roleIds: { set: [] } } }) + expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(2, { where: expect.any(Object), data: { roleIds: { set: [members[1].roleIds[1]] } } }) + expect(prisma.projectRole.delete).toHaveBeenCalledWith({ where: { id: roleId } }) + }) + }) + describe.skip('countRolesMembers', () => { + it('should return aggregated role member counts', async () => { + const partialRoles = [{ + id: faker.string.uuid(), + }, { + id: faker.string.uuid(), + }] as const satisfies Partial[] + + const users = [{ + projectRoleIds: [partialRoles[0].id, partialRoles[1].id], + }, { + projectRoleIds: [partialRoles[1].id], + }] as const satisfies Partial[] + prisma.projectRole.findMany.mockResolvedValue(partialRoles) + prisma.user.findMany.mockResolvedValue(users) + + const response = await countRolesMembers() + + expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 }) + }) + }) + describe('patchRoles', () => { + const dbRoles: ProjectRole[] = [{ + id: faker.string.uuid(), + name: faker.company.name(), + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 0, + projectId, + }, { + id: faker.string.uuid(), + name: faker.company.name(), + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 1, + projectId, + }] + + it('should do nothing', async () => { + prisma.projectRole.findMany.mockResolvedValue([]) + await patchRoles(projectId, []) + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) + }) + + it('should return 400 if incoherent positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 1 }, + ] + prisma.projectRole.findMany.mockResolvedValue(dbRoles) + + const response = await patchRoles(projectId, updateRoles) + + expect(response).instanceOf(BadRequest400) + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) + }) + + it('should return 400 if incoherent positions (missing)', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, position: 1 }, + ] + prisma.projectRole.findMany.mockResolvedValue(dbRoles) + + const response = await patchRoles(projectId, updateRoles) + + expect(response).instanceOf(BadRequest400) + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) + }) + + it('should update positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 0 }, + ] + prisma.projectRole.findMany.mockResolvedValue(dbRoles) + + await patchRoles(projectId, updateRoles) + + expect(prisma.projectRole.update).toHaveBeenCalledTimes(2) + }) + + it('should update permissions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, permissions: '0' }, + ] + prisma.projectRole.findMany.mockResolvedValue(dbRoles) + + await patchRoles(projectId, updateRoles) + + expect(prisma.projectRole.update).toHaveBeenCalledTimes(1) + expect(prisma.projectRole.update).toHaveBeenCalledWith({ + data: { + name: dbRoles[1].name, + permissions: 0n, + position: 1, + }, + where: { + id: dbRoles[1].id, + }, + }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts new file mode 100644 index 000000000..3d20dc13c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts @@ -0,0 +1,77 @@ +import type { projectRoleContract } from '@cpn-console/shared' +import type { Project, ProjectRole } from '@prisma/client' +import { + deleteRole as deleteRoleQuery, + listMembers, + listRoles as listRolesQuery, + updateRole, +} from '@/resources/queries-index.js' +import { BadRequest400 } from '@/utils/errors.js' +import prisma from '@/prisma.js' + +export async function listRoles(projectId: Project['id']) { + return listRolesQuery(projectId) + .then(roles => roles.map(role => ({ ...role, permissions: role.permissions.toString() }))) +} + +export async function patchRoles(projectId: Project['id'], roles: typeof projectRoleContract.patchProjectRoles.body._type) { + const dbRoles = await listRoles(projectId) + const positionsAvailable: number[] = [] + + const updatedRoles = dbRoles + .filter(dbRole => roles.find(role => role.id === dbRole.id)) // filter non concerned dbRoles + .map((dbRole) => { + const matchingRole = roles.find(role => role.id === dbRole.id) + if (typeof matchingRole?.position !== 'undefined' && !positionsAvailable.includes(matchingRole.position)) { + positionsAvailable.push(matchingRole.position) + } + return { + id: matchingRole?.id ?? dbRole.id, + name: matchingRole?.name ?? dbRole.name, + permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : BigInt(dbRole.permissions), + position: matchingRole?.position ?? dbRole.position, + } + }) + if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes') + for (const { id, ...role } of updatedRoles) { + await updateRole(id, role) + } + + return listRoles(projectId) +} + +export async function createRole(projectId: Project['id'], role: typeof projectRoleContract.createProjectRole.body._type) { + const dbMaxPosRole = (await prisma.projectRole.findFirst({ + where: { projectId }, + orderBy: { position: 'desc' }, + select: { position: true }, + }))?.position ?? -1 + + await prisma.projectRole.create({ + data: { + ...role, + projectId, + position: dbMaxPosRole + 1, + permissions: BigInt(role.permissions), + }, + }) + + return listRoles(projectId) +} + +export async function countRolesMembers(projectId: Project['id']) { + const roles = await listRoles(projectId) + const members = await listMembers(projectId) + const rolesCounts: Record = Object.fromEntries(roles.map(role => [role.id, 0])) // {role uuid: 0} + for (const { roleIds } of members) { + for (const roleId of roleIds) { + rolesCounts[roleId]++ + } + } + return rolesCounts +} + +export async function deleteRole(roleId: Project['id']) { + await deleteRoleQuery(roleId) + return null +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts new file mode 100644 index 000000000..d915849eb --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts @@ -0,0 +1,54 @@ +import type { + Prisma, + Project, + + ProjectRole, +} from '@prisma/client' + +import prisma from '@/prisma.js' + +export const listRoles = (projectId: Project['id']) => prisma.projectRole.findMany({ where: { projectId }, orderBy: { position: 'asc' } }) + +export function createRole(data: Pick) { + return prisma.projectRole.create({ + data: { + name: data.name, + permissions: 0n, + position: data.position, + projectId: data.projectId, + }, + }) +} + +export function updateRole(id: ProjectRole['id'], data: Pick) { + return prisma.projectRole.update({ + where: { id }, + data, + }) +} + +export async function deleteRole(id: ProjectRole['id']) { + const role = await prisma.projectRole.delete({ + where: { + id, + }, + }) + const attachedMembers = await prisma.projectMembers.findMany({ + where: { projectId: role.projectId, roleIds: { has: id } }, + }) + for (const member of attachedMembers) { + await prisma.projectMembers.update({ + where: { + projectId_userId: { + projectId: role.projectId, + userId: member.userId, + }, + }, + data: { + roleIds: { + set: member.roleIds.filter(roleId => roleId !== id), + }, + }, + }) + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts new file mode 100644 index 000000000..f6a0539c7 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts @@ -0,0 +1,316 @@ +import { faker } from '@faker-js/faker' +import { PROJECT_PERMS, projectRoleContract } from '@cpn-console/shared' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' +import { BadRequest400 } from '../../utils/errors.js' +import * as business from './business.js' + +vi.mock('./business.js') +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) + +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessCreateRoleMock = vi.spyOn(business, 'createRole') +const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole') +const businessListRolesMock = vi.spyOn(business, 'listRoles') +const businessPatchRolesMock = vi.spyOn(business, 'patchRoles') +const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers') + +describe('tests projectRoleContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + const projectId = faker.string.uuid() + const roleId = faker.string.uuid() + + describe('listProjectRoles', () => { + it('should return roles for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessListRolesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectRoleContract.listProjectRoles.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectRoleContract.listProjectRoles.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + }) + + describe('createProjectRole', () => { + it('should create role for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(201) + }) + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) + + describe('patchProjectRoles', () => { + it('should patch roles for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessPatchRolesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessPatchRolesMock.mockResolvedValue(new BadRequest400('une erreur')) + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('projectRoleMemberCounts', () => { + it('should return member counts for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCountRolesMembersMock.mockResolvedValueOnce({}) + + const response = await app.inject() + .get(projectRoleContract.projectRoleMemberCounts.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual({}) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectRoleContract.projectRoleMemberCounts.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + }) + + describe('deleteProjectRole', () => { + it('should delete role for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteRoleMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts new file mode 100644 index 000000000..2d55a2b84 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts @@ -0,0 +1,90 @@ +import { AdminAuthorized, ProjectAuthorized, projectRoleContract } from '@cpn-console/shared' +import { + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403, NotFound404 } from '@/utils/errors.js' + +export function projectRoleRouter() { + return serverInstance.router(projectRoleContract, { + // Récupérer des projets + listProjectRoles: async ({ request: req, params }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + const body = await listRoles(projectId) + + return { + status: 200, + body, + } + }, + + createProjectRole: async ({ request: req, params: { projectId }, body }) => { + const perms = await authUser(req, { id: projectId }) + + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await createRole(projectId, body) + + return { + status: 201, + body: resBody, + } + }, + + patchProjectRoles: async ({ request: req, params: { projectId }, body }) => { + const perms = await authUser(req, { id: projectId }) + + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await patchRoles(projectId, body) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 200, + body: resBody, + } + }, + + projectRoleMemberCounts: async ({ request: req, params }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + const resBody = await countRolesMembers(projectId) + + return { + status: 200, + body: resBody, + } + }, + + deleteProjectRole: async ({ request: req, params: { projectId, roleId } }) => { + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await deleteRole(roleId) + + return { + status: 204, + body: resBody, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts new file mode 100644 index 000000000..e11890d8e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts @@ -0,0 +1,95 @@ +import type { Project, ProjectPlugin } from '@prisma/client' +import type { + PermissionTarget, + PluginsUpdateBody, + ServiceUrl, +} from '@cpn-console/shared' +import { editStrippers, populatePluginManifests, servicesInfos } from '@cpn-console/hooks' +import type { ZoneObject } from '@cpn-console/hooks' +import { + getAdminPlugin, + getProjectInfosByIdOrThrow, + getProjectStore, + getPublicClusters, + saveProjectStore, +} from '@/resources/queries-index.js' + +export type ConfigRecords = { + key: string + pluginName: string + value: string | number | null +}[] + +export function dbToObj(records: Omit[]): PluginsUpdateBody { + const obj: PluginsUpdateBody = {} + for (const record of records) { + if (!obj[record.pluginName]) obj[record.pluginName] = {} + obj[record.pluginName][record.key] = record.value + } + return obj +} + +export function objToDb(obj: PluginsUpdateBody): ConfigRecords { + return Object.entries(obj) + .map(([pluginName, values]) => Object.entries(values) + .map(([key, value]) => ({ pluginName, key, value }))) + .flat() +} + +export async function getProjectServices(projectId: Project['id'], permissionTarget: PermissionTarget) { + // Pré-requis + const project = await getProjectInfosByIdOrThrow(projectId) + + const [projectStore, globalConfig] = await Promise.all([ + getProjectStore(projectId), + getAdminPlugin(), + ]) + const store = dbToObj([...projectStore, ...globalConfig]) + + const publicClusters = await getPublicClusters() + project.clusters = project.clusters.concat(publicClusters) + const zones: Map = new Map() // Pour dédoublonnage des zones + project.clusters.map(c => zones.set(c.zone.id, c.zone)) + + return Object.values(servicesInfos).map(({ name, title, to, imgSrc, description }) => { + let urls: ServiceUrl[] = [] + const toResponse = to + ? to({ + clusters: project.clusters, + zones: Array.from(zones.values()), + environments: project.environments, + project, + store, + }) + : [] + if (Array.isArray(toResponse)) { + urls = toResponse.map(res => ({ name: res.title ?? '', description: res.description ?? '', to: res.to })) + } else if (typeof toResponse === 'string') { + urls = [{ to: toResponse, name: '' }] + } else if (toResponse) { + urls = [{ name: toResponse.title ?? '', to: toResponse.to }] + } + const manifest = populatePluginManifests({ + data: { + project: projectStore, + global: globalConfig, + }, + permissionTarget, + pluginName: name, + select: { + global: true, + project: true, + }, + }) + return { imgSrc, title, name, urls, manifest, description } + }).filter(s => s.urls.length || s.manifest.global?.length || s.manifest.project?.length) +} + +export async function updateProjectServices(projectId: Project['id'], data: PluginsUpdateBody, stripperRoles: Array<'user' | 'admin'>) { + for (const role of stripperRoles) { + const parsedData = editStrippers.project[role].safeParse(data) + if (!parsedData.success) continue + await saveProjectStore(objToDb(parsedData.data), projectId) + } + return null +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts new file mode 100644 index 000000000..cf353614b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts @@ -0,0 +1,54 @@ +import type { Project } from '@prisma/client' +import type { ConfigRecords } from './business.js' +import prisma from '@/prisma.js' + +// CONFIG +export function getProjectStore(projectId: Project['id']) { + return prisma.projectPlugin.findMany({ + where: { projectId }, + select: { + key: true, + pluginName: true, + value: true, + }, + }) +} + +export const getAdminPlugin = prisma.adminPlugin.findMany + +export async function saveProjectStore(records: ConfigRecords, projectId: Project['id']) { + for (const { pluginName, key, value } of records) { + if (value === null) { + await prisma.projectPlugin.delete({ + where: { + projectId_pluginName_key: { + projectId, + pluginName, + key, + }, + }, + }) + } else { + await prisma.projectPlugin.upsert({ + create: { + pluginName, + projectId, + key, + value: value.toString(), + }, + update: { + key, + value: value.toString(), + pluginName, + }, + where: { + projectId_pluginName_key: { + projectId, + pluginName, + key, + }, + }, + }) + } + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts new file mode 100644 index 000000000..8df654e90 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts @@ -0,0 +1,160 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PROJECT_PERMS, projectServiceContract } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetServicesMock = vi.spyOn(business, 'getProjectServices') +const businessUpdateServicesMock = vi.spyOn(business, 'updateProjectServices') + +describe('projectServiceRouter tests', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + const projectId = faker.string.uuid() + + describe('getServices', () => { + it('should return services for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetServicesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) + .query({ permissionTarget: 'user' }) + .end() + + expect(businessGetServicesMock).toHaveBeenCalledWith(projectId, 'user') + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should not return admin services for non admin', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetServicesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) + .query({ permissionTarget: 'admin' }) + .end() + + expect(businessGetServicesMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + + it('should return services for admin', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetServicesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) + .end() + + expect(businessGetServicesMock).toHaveBeenCalledWith(projectId, 'user') + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + }) + + describe('updateProjectServices', () => { + const updateData = { serviceA: { param1: 'value' } } + + it('should update services for project manager', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateServicesMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(businessUpdateServicesMock).toHaveBeenCalledWith(projectId, updateData, ['user']) + expect(response.statusCode).toEqual(204) + }) + + it('should update services for project admin', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateServicesMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(businessUpdateServicesMock).toHaveBeenCalledWith(projectId, updateData, ['user', 'admin']) + expect(response.statusCode).toEqual(204) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts new file mode 100644 index 000000000..e79ccb4ac --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts @@ -0,0 +1,38 @@ +import { AdminAuthorized, ProjectAuthorized, projectServiceContract } from '@cpn-console/shared' +import { getProjectServices, updateProjectServices } from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { Forbidden403, NotFound404 } from '@/utils/errors.js' + +export function projectServiceRouter() { + return serverInstance.router(projectServiceContract, { + // Récupérer les services d'un projet + getServices: async ({ request: req, params: { projectId }, query }) => { + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!AdminAuthorized.isAdmin(perms.adminPermissions) && query.permissionTarget === 'admin') return new Forbidden403('Vous ne pouvez pas demander les paramètres admin') + + const body = await getProjectServices(projectId, query.permissionTarget) + + return { + status: 200, + body, + } + }, + + updateProjectServices: async ({ request: req, params: { projectId }, body }) => { + const perms = await authUser(req, { id: projectId }) + if (!ProjectAuthorized.Manage(perms)) return new NotFound404() + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + + const allowedRoles: Array<'user' | 'admin'> = AdminAuthorized.isAdmin(perms.adminPermissions) ? ['user', 'admin'] : ['user'] + + const resBody = await updateProjectServices(projectId, body, allowedRoles) + return { + status: 204, + body: resBody, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts new file mode 100644 index 000000000..5d064cd5a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts @@ -0,0 +1,361 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Cluster, Project, ProjectMembers, ProjectRole, User } from '@prisma/client' +import prisma from '../../__mocks__/prisma.js' +import { hook } from '../../__mocks__/utils/hook-wrapper.ts' +import { dbToObj } from '../project-service/business.ts' +import * as userBusiness from '../user/business.js' +import { + BadRequest400, + ErrorResType, + Unprocessable422, +} from '../../utils/errors.js' +import { archiveProject, chunk, createProject, generateProjectsData, generateSlug, getProjectSecrets, listProjects, replayHooks, updateProject } from './business.ts' + +vi.mock('../../utils/hook-wrapper.ts', async () => ({ + hook, +})) + +const logViaSessionMock = vi.spyOn(userBusiness, 'logViaSession') + +const projectId = faker.string.uuid() + +const user: User = { + id: faker.string.uuid(), + createdAt: new Date(), + updatedAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + adminRoleIds: [], + type: 'human', + lastLogin: null, +} +const project: Project & { + clusters: Pick[] + members: ProjectMembers[] + roles: ProjectRole[] + owner: User +} = { + createdAt: new Date(), + updatedAt: new Date(), + description: '', + everyonePerms: 649n, + id: faker.string.uuid(), + locked: false, + name: faker.string.alphanumeric(8), + status: 'created', + ownerId: faker.string.uuid(), + clusters: [], + roles: [], + members: [], +} +const reqId = faker.string.uuid() +describe('test project business utils', () => { + it('should transform arrow ', async () => { + const result = dbToObj([{ key: 'test', pluginName: 'test', value: 'test' }]) + expect(result).toEqual({ test: { test: 'test' } }) + }) +}) + +describe('test project business logic', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + describe('listProjects', () => { + it('should return stringified perms', async () => { + prisma.project.findMany.mockResolvedValue([{ everyonePerms: 5n, clusters: [], roles: [{ permissions: 28n }] }]) + const response = await listProjects({}, user.id) + expect(response[0].everyonePerms).toBe('5') + expect(response[0].roles[0].permissions).toBe('28') + }) + }) + describe('getProjectSecrets', () => { + const getResultsHook = { + failed: false, + args: {}, + results: { + registry: { + secrets: { + token: 'myToken', + }, + status: { + failed: false, + }, + }, + }, + } + it('should return transform secret', async () => { + hook.project.getSecrets.mockResolvedValue(getResultsHook) + + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) + const response = await getProjectSecrets(projectId) + + // according to src/utils/mocks.ts + expect(JSON.stringify(response)).toContain('myToken') + }) + + it('should return projects secrets', async () => { + hook.project.getSecrets.mockResolvedValue(getResultsHook) + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) + prisma.project.findMany.mockResolvedValue({ id: projectId }) + const response = await getProjectSecrets(projectId) + // according to src/utils/mocks.ts + expect(JSON.stringify(response)).toContain('myToken') + }) + + it('should return hook error', async () => { + hook.project.getSecrets.mockResolvedValue({ failed: true }) + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) + prisma.project.findMany.mockResolvedValue({ id: projectId }) + const response = await getProjectSecrets(projectId) + // according to src/utils/mocks.ts + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + + describe('createProject', () => { + it('should create project', async () => { + logViaSessionMock.mockResolvedValue({ user }) + + prisma.project.create.mockResolvedValue({ ...project, status: 'initializing' }) + prisma.project.findFirst.mockResolvedValue(undefined) + prisma.project.findMany.mockResolvedValue([]) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const projectRes = await createProject(project, user, reqId) + + expect(projectRes.name).toEqual(project.name) + expect(prisma.project.create).toHaveBeenCalledTimes(1) + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + }) + + it('should return plugins failed', async () => { + logViaSessionMock.mockResolvedValue({ user }) + + prisma.project.create.mockResolvedValue({ ...project, status: 'initializing' }) + prisma.project.findFirst.mockResolvedValue(undefined) + prisma.project.findMany.mockResolvedValue([]) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const response = await createProject(project, user, reqId) + + expect(prisma.project.create).toHaveBeenCalledTimes(1) + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + describe('updateProject', () => { + const updatedProjet = { + description: faker.lorem.lines(2), + everyonePerms: '5', + } + const reqId = faker.string.uuid() + const members: ProjectMembers[] = [{ userId: faker.string.uuid(), projectId: project.id, roleIds: [], user: { type: 'human' } }] + it('should update project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members }) + prisma.project.update.mockResolvedValue(project) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + await updateProject({ ...updatedProjet, ownerId: members[0].userId }, project.id, user, reqId) + + expect(prisma.project.update).toHaveBeenCalledTimes(2) + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + }) + + it('should update nothing', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members }) + prisma.project.update.mockResolvedValue(project) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + await updateProject({ }, project.id, user, reqId) + + expect(prisma.project.update).toHaveBeenCalledTimes(0) + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + }) + + it('should not update if project archived', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, status: 'archived' }) + prisma.project.update.mockResolvedValue(project) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const response = await updateProject({ }, project.id, user, reqId) + + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.project.update).toHaveBeenCalledTimes(0) + expect(prisma.log.create).toHaveBeenCalledTimes(0) + expect(hook.project.upsert).toHaveBeenCalledTimes(0) + }) + + it('should not update project, cause missing member', async () => { + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + logViaSessionMock.mockResolvedValue({ user }) + + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members: [] }) + + const response = await updateProject({ ownerId: members[0].userId }, project.id, user, reqId) + + expect(prisma.project.findUniqueOrThrow).toHaveBeenCalledTimes(1) + expect(response).toBeInstanceOf(BadRequest400) + expect(hook.project.upsert).toHaveBeenCalledTimes(0) + expect(prisma.log.update).toHaveBeenCalledTimes(0) + }) + + it('should return plugins failed', async () => { + logViaSessionMock.mockResolvedValue({ user }) + + prisma.project.findUniqueOrThrow.mockResolvedValue({ status: 'created' }) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const response = await updateProject(updatedProjet, project.id, user, reqId) + + expect(prisma.project.update).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + describe('replayHooks', () => { + const reqId = faker.string.uuid() + + it('should replay hooks', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'created' }) + hook.project.upsert.mockResolvedValue({ results: { failed: false } }) + + await replayHooks(project.id, user, reqId) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + }) + + it('should not replay hooks on archived project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'archived' }) + hook.project.upsert.mockResolvedValue({ results: { failed: false } }) + + const response = await replayHooks(project.id, user, reqId) + + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.log.create).toHaveBeenCalledTimes(0) + expect(hook.project.upsert).toHaveBeenCalledTimes(0) + }) + + it('should not replay hooks on locked project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: true, status: 'created' }) + hook.project.upsert.mockResolvedValue({ results: { failed: false } }) + + const response = await replayHooks(project.id, user, reqId) + + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.log.create).toHaveBeenCalledTimes(0) + expect(hook.project.upsert).toHaveBeenCalledTimes(0) + }) + + it('should update nothing and return error', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'created' }) + hook.project.upsert.mockResolvedValue({ results: { failed: true } }) + + const response = await replayHooks(project.id, user, reqId) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + + describe('archiveProject', () => { + it('should archive project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false }) + hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) + const response = await archiveProject(project.id, user, reqId) + expect(response).toBeNull() + expect(prisma.project.update).toHaveBeenLastCalledWith({ + where: { id: project.id }, + data: { + clusters: { set: [] }, + }, + }) + }) + + it('should not archive a project already archived', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false, status: 'archived' }) + hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) + const response = await archiveProject(project.id, user, reqId) + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.project.update).toHaveBeenCalledTimes(0) + }) + + it('should not archive a project locked', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: true, status: 'created' }) + hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) + const response = await archiveProject(project.id, user, reqId) + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.project.update).toHaveBeenCalledTimes(0) + }) + + it('should return hook fail', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false }) + hook.project.delete.mockResolvedValue({ results: { failed: true }, project: Promise.resolve({ status: 'failed' }) }) + const response = await archiveProject(project.id, user, reqId) + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + + describe('generateProjectsData', () => { + it('shoud return string, very bad test ...', async () => { + prisma.project.findMany.mockResolvedValue([{ name: 'test' }]) + const response = await generateProjectsData() + expect(response).toBeTypeOf('string') + }) + }) +}) + +describe('chunk function', () => { + it('should return 5 elements', () => { + const letters = ['A', 'B', 'C', 'D', 'E'] + expect(chunk(letters, 5)).toEqual([letters]) + }) + it('should return 3,2 elements', () => { + const letters = ['A', 'B', 'C', 'D', 'E'] + expect(chunk(letters, 3)).toEqual([['A', 'B', 'C'], ['D', 'E']]) + }) + it('should return 4 elements', () => { + const letters = ['A', 'B', 'C', 'D'] + expect(chunk(letters, 5)).toEqual([letters]) + }) +}) + +describe('generateSlug', () => { + it('should return prefix, no array', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix) + expect(generated).toEqual(prefix) + }) + it('should return prefix, empty array', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, []) + expect(generated).toEqual(prefix) + }) + it('should return prefix, no match', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, [faker.string.alphanumeric(5), faker.string.alphanumeric(5)]) + expect(generated).toEqual(prefix) + }) + it('should return generated slug at 1 or 0, all matchs', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, [prefix]) + expect(generated).match(/-[01]$/) + }) + it('should return generated slug at 4, all matchs', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, [prefix, `${prefix}-0`, `${prefix}-1`, `${prefix}-2`, `${prefix}-3`]) + expect(generated).match(/-4$/) + }) + it('should fill empty space', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, [prefix, `${prefix}-0`, `${prefix}-1`, `${prefix}-3`]) + expect(generated).match(/-2$/) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts new file mode 100644 index 000000000..e81ac8d3e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts @@ -0,0 +1,261 @@ +import { json2csv } from 'json-2-csv' +import { servicesInfos } from '@cpn-console/hooks' +import type { Project, User } from '@prisma/client' +import type { projectContract } from '@cpn-console/shared' +import { ProjectStatusSchema } from '@cpn-console/shared' +import { + addLogs, + deleteAllEnvironmentForProject, + deleteAllRepositoryForProject, + getAllProjectsDataForExport, + getProjectOrThrow, + getSlugs, + initializeProject, + listProjects as listProjectsQuery, + lockProject, + updateProject as updateProjectQuery, +} from '@/resources/queries-index.js' +import type { ErrorResType } from '@/utils/errors.js' +import { BadRequest400, Forbidden403, Unprocessable422 } from '@/utils/errors.js' +import { whereBuilder } from '@/utils/controller.js' +import { hook } from '@/utils/hook-wrapper.js' +import type { UserDetails } from '@/types/index.js' +import prisma from '@/prisma.js' +import { parallelBulkLimit } from '@/utils/env.js' + +export function generateSlug(prefix: string, existingSlugs?: string[]) { + if (!existingSlugs?.includes(prefix)) { + return prefix + } + let idx = 1 + let generated = `${prefix}-${idx}` + while (existingSlugs.includes(generated)) { + idx++ + generated = `${prefix}-${idx}` + } + return generated +} + +const projectStatus = ProjectStatusSchema._def.values +export async function listProjects({ status, statusIn, statusNotIn, filter = 'member', ...query }: typeof projectContract.listProjects.query._type, userId: User['id'] | undefined) { + return listProjectsQuery({ + ...query, + status: whereBuilder({ enumValues: projectStatus, eqValue: status, inValues: statusIn, notInValues: statusNotIn }), + filter, + userId, + }).then(projects => projects.map(({ clusters, ...project }) => ({ + ...project, + clusterIds: clusters.map(({ id }) => id), + roles: project.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), + everyonePerms: project.everyonePerms.toString(), + }))) +} + +export async function getProjectSecrets(projectId: string) { + const hookReply = await hook.project.getSecrets(projectId) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la récupération des secrets du projet') + } + + return Object.fromEntries( + Object.entries(hookReply.results) + // @ts-ignore + .filter(([_key, value]) => Object.keys(value.secrets).length) + // @ts-ignore + .map(([key, value]) => [servicesInfos[key]?.title, value.secrets]), + ) +} + +export async function createProject(dataDto: typeof projectContract.createProject.body._type, requestor: UserDetails, requestId: string) { + if (requestor.type !== 'human') return new BadRequest400('Seuls les comptes humains peuvent créer des projets') + + let slug = dataDto.name + const projectsWithSamePrefix = await getSlugs(slug) + slug = generateSlug(slug, projectsWithSamePrefix?.map(project => project.slug)) + + // Actions + const project = await initializeProject({ ...dataDto, slug, ownerId: requestor.id }) + + const { results, project: projectInfos } = await hook.project.upsert(project.id) + await addLogs({ action: 'Create Project', data: results, userId: requestor.id, requestId, projectId: project.id }) + if (results.failed) { + return new Unprocessable422('Echec des services à la création du projet') + } + + return { + ...projectInfos, + clusterIds: projectInfos.clusters.map(({ id }) => id), + everyonePerms: projectInfos.everyonePerms.toString(), + roles: projectInfos.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), + } +} + +export async function getProject(projectId: Project['id']) { + return getProjectOrThrow(projectId).then(({ clusters, ...project }) => ({ + ...project, + clusterIds: clusters.map(({ id }) => id), + roles: project.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), + everyonePerms: project.everyonePerms.toString(), + })) +} + +export async function updateProject( + { description, ownerId: ownerIdCandidate, everyonePerms, locked, ...data }: typeof projectContract.updateProject.body._type, + projectId: Project['id'], + requestor: UserDetails, + requestId: string, +) { + // Actions + const projectDb = await prisma.project.findUniqueOrThrow({ + where: { id: projectId }, + include: { members: { include: { user: true } } }, + }) + + if (projectDb.status === 'archived') return new Forbidden403('Le projet est archivé') + + if (ownerIdCandidate && ownerIdCandidate !== projectDb.ownerId) { + const memberCandidate = projectDb.members.find(member => member.userId === ownerIdCandidate) + if (!memberCandidate) { + return new BadRequest400('Le nouveau propriétaire doit faire partie des membres actuels du projet') + } + if (memberCandidate.user.type !== 'human') return new BadRequest400('Seuls les comptes humains peuvent être propriétaire de projets') + if (!projectDb.members.find(member => member.userId === projectDb.ownerId)) { + await prisma.projectMembers.create({ + data: { userId: projectDb.ownerId, projectId }, + }) + } + await prisma.$transaction([ + prisma.projectMembers.delete({ + where: { projectId_userId: { userId: ownerIdCandidate, projectId } }, + }), + prisma.project.update({ where: { id: projectId }, data: { ownerId: ownerIdCandidate } }), + ]) + } + + if (typeof description !== 'undefined' || typeof everyonePerms !== 'undefined' || typeof locked !== 'undefined') { + await updateProjectQuery(projectId, { + description, + locked, + ...everyonePerms && { everyonePerms: BigInt(everyonePerms) }, + ...data, + }) + } + + const { results, project: projectInfos } = await hook.project.upsert(projectId) + await addLogs({ action: 'Update Project', data: results, userId: requestor.id, requestId, projectId: projectInfos.id }) + if (results.failed) { + return new Unprocessable422('Echec des services à la mise à jour du projet') + } + + return { + ...projectInfos, + clusterIds: projectInfos.clusters.map(({ id }) => id), + everyonePerms: projectInfos.everyonePerms.toString(), + roles: projectInfos.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), + } +} + +interface ReplayHooksArgs { + projectId: Project['id'] + userId?: User['id'] + requestId: string +} +export async function replayHooks({ projectId, userId, requestId }: ReplayHooksArgs): Promise { + const projectDb = await prisma.project.findUniqueOrThrow({ + where: { id: projectId }, + include: { members: { include: { user: true } } }, + }) + if (projectDb.locked) return new Forbidden403('Le projet est verrouillé') + if (projectDb.status === 'archived') return new Forbidden403('Le projet est archivé') + // Actions + const { results } = await hook.project.upsert(projectId) + await addLogs({ action: 'Replay hooks for Project', data: results, userId, requestId, projectId }) + if (results.failed) { + return new Unprocessable422('Echec des services au reprovisionnement du projet') + } + return null +} + +export async function archiveProject(projectId: Project['id'], requestor: UserDetails, requestId: string): Promise { + // Actions + // Empty the project first + const [projectDb, ..._] = await Promise.all([ + // get initial project state + prisma.project.findUniqueOrThrow({ where: { id: projectId } }), + deleteAllRepositoryForProject(projectId), + deleteAllEnvironmentForProject(projectId), + ]) + + if (projectDb.locked) return new Forbidden403('Le projet est verrouillé') + if (projectDb.status === 'archived') return new BadRequest400('Le projet est archivé') + if (projectDb.locked) { + await lockProject(projectId) + } + + // -- début - Suppression projet -- + const { results, project } = await hook.project.delete(projectId) + await addLogs({ action: 'Delete all project resources', data: results, userId: requestor.id, requestId, projectId }) + if (project.status !== 'archived' && !projectDb.locked) { + await prisma.project.update({ where: { id: projectId }, data: { locked: false } }) + } + if (results.failed) { + return new Unprocessable422('Echec des services à la suppression du projet') + } + + // Retrait clusters -- + await prisma.project.update({ + where: { id: projectId }, + data: { + clusters: { set: [] }, + }, + }) + + // -- fin - Suppression projet -- + return null +} + +export async function generateProjectsData() { + const projects = await getAllProjectsDataForExport() + + return json2csv(projects, { + emptyFieldValue: '', + }) +} + +export async function bulkActionProject(data: typeof projectContract.bulkActionProject.body._type, requestor: UserDetails, requestId: string) { + if (data.projectIds === 'all') { + data.projectIds = (await prisma.project.findMany({ + select: { id: true }, + where: { status: { not: 'archived' } }, + })).map(({ id }) => id) + } + bulkExector(data.projectIds + .map((projectId) => { + if (data.action === 'archive') { + return () => archiveProject(projectId, requestor, requestId) + } + if (data.action === 'lock') { + return () => updateProject({ locked: true }, projectId, requestor, requestId) + } + if (data.action === 'unlock') { + return () => updateProject({ locked: false }, projectId, requestor, requestId) + } + if (data.action === 'replay') { + return () => replayHooks({ projectId, userId: requestor.id, requestId }) + } + // should never been called + return async () => {} + })) +} + +export function chunk(arr: T[], size: number): T[][] { + return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => + arr.slice(i * size, i * size + size)) +} + +async function bulkExector(toExecute: Array<() => Promise>) { + const toExecuteChunked = chunk(toExecute, parallelBulkLimit) + for (const chunkToExecute of toExecuteChunked) { + await Promise.allSettled(chunkToExecute.map(fn => fn())) + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts new file mode 100644 index 000000000..23544d1d2 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts @@ -0,0 +1,335 @@ +import type { + Prisma, + Project, + User, +} from '@prisma/client' +import { + ProjectStatus, +} from '@prisma/client' +import type { XOR, projectContract } from '@cpn-console/shared' +import prisma from '@/prisma.js' +import { appVersion } from '@/utils/env.js' +import { uuid } from '@/utils/queries-tools.js' + +type ProjectUpdate = Partial> +export function updateProject(id: Project['id'], data: ProjectUpdate) { + return prisma.project.update({ + where: { id }, + data, + include: { members: true }, + }) +} + +// SELECT +type FilterWhere = XOR<{ + userId?: User['id'] + filter: 'all' +}, { + userId: User['id'] | undefined + filter: 'owned' | 'member' + }> +type ListProjectWhere = Omit<(typeof projectContract.listProjects.query._type), 'status_in' | 'status_not_in' | 'status'> & + Pick & + FilterWhere +export async function listProjects({ + description, + locked, + name, + status, + id, + filter, + userId, + search, + lastSuccessProvisionningVersion, +}: ListProjectWhere) { + const whereAnd: Prisma.ProjectWhereInput[] = [] + if (id) whereAnd.push({ id }) + if (locked != null) whereAnd.push({ locked }) + if (name) whereAnd.push({ name }) + if (status) whereAnd.push({ status }) + if (description) whereAnd.push({ description: { contains: description } }) + if (lastSuccessProvisionningVersion) { + if (lastSuccessProvisionningVersion === 'outdated') whereAnd.push({ lastSuccessProvisionningVersion: { not: appVersion } }) + else if (lastSuccessProvisionningVersion === 'last') whereAnd.push({ lastSuccessProvisionningVersion: { equals: appVersion } }) + else whereAnd.push({ lastSuccessProvisionningVersion }) + } + if (search) { + whereAnd.push({ OR: [{ + name: { contains: search }, + }, { + owner: { email: { contains: search } }, + }] }) + } + + if (filter === 'owned') { + whereAnd.push({ ownerId: userId }) + } else if (filter === 'member') { + whereAnd.push({ OR: [{ + members: { some: { userId } }, + }, { + ownerId: userId, + }] }) + } + + return prisma.project.findMany({ + where: { AND: whereAnd }, + include: { + clusters: { select: { id: true } }, + members: { include: { user: true } }, + roles: true, + owner: true, + }, + }) +} + +export function getProjectOrThrow(id: Project['id'] | Project['slug']) { + return prisma.project.findFirstOrThrow({ + where: uuid.test(id) + ? { id } + : { slug: id }, + include: { + clusters: { select: { id: true } }, + members: { include: { user: true } }, + roles: true, + owner: true, + }, + }) +} + +export function getProjectInfosByIdOrThrow(projectId: Project['id']) { + return prisma.project.findUniqueOrThrow({ + where: { + id: projectId, + }, + include: { + environments: true, + clusters: { include: { zone: true } }, + }, + }) +} + +export function getProjectMembers(projectId: Project['id']) { + return prisma.projectMembers.findMany({ + where: { + projectId, + }, + include: { user: true }, + }) +} + +export function getProjectById(id: Project['id']) { + return prisma.project.findUnique({ where: { id } }) +} + +export const baseProjectIncludes = { + members: { include: { user: true } }, + clusters: true, + roles: true, + owner: true, +} as const + +export function getProjectInfos(id: Project['id']) { + return prisma.project.findUnique({ + where: { id }, + include: baseProjectIncludes, + }) +} + +export function getProjectInfosOrThrow(id: Project['id']) { + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: baseProjectIncludes, + }) +} + +export function getProjectInfosAndRepos(id: Project['id']) { + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: { + ...baseProjectIncludes, + repositories: true, + }, + }) +} + +export function getSlugs(slugPrefix: string) { + return prisma.project.findMany({ + where: { + slug: { startsWith: slugPrefix }, + }, + }) +} + +export function getAllProjectsDataForExport() { + return prisma.project.findMany({ + select: { + name: true, + description: true, + createdAt: true, + updatedAt: true, + environments: { + select: { + name: true, + stage: true, + cluster: { + select: { label: true }, + }, + }, + }, + owner: true, + }, + }) +} + +export function getRolesByProjectId(projectId: Project['id']) { + return prisma.projectRole.findMany({ + where: { projectId }, + }) +} + +const clusterInfosSelect = { + id: true, + infos: true, + label: true, + external: true, + privacy: true, + secretName: true, + kubeconfig: true, + clusterResources: true, + cpu: true, + gpu: true, + memory: true, + zone: { + select: { + id: true, + slug: true, + argocdUrl: true, + label: true, + }, + }, +} +export function getHookProjectInfos(id: Project['id']) { + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: { + members: { include: { user: true }, where: { user: { type: 'human' } } }, + clusters: { select: clusterInfosSelect }, + environments: { + include: { + stage: true, + cluster: { + select: clusterInfosSelect, + }, + }, + }, + repositories: true, + plugins: { + select: { + key: true, + pluginName: true, + value: true, + }, + }, + owner: true, + roles: true, + }, + }) +} + +// CREATE +interface CreateProjectParams { + name: Project['name'] + description?: Project['description'] + ownerId: User['id'] + slug: Project['slug'] + limitless: boolean + hprodCpu: number + hprodGpu: number + hprodMemory: number + prodCpu: number + prodGpu: number + prodMemory: number +} + +export function initializeProject(params: CreateProjectParams) { + return prisma.project.create({ + data: { + description: params.description ?? '', + status: ProjectStatus.created, + locked: false, + ...params, + }, + }) +} + +// UPDATE +export function lockProject(id: Project['id']) { + return prisma.project.update({ + where: { id }, + data: { locked: true }, + }) +} + +export function updateProjectCreated(id: Project['id']) { + return prisma.project.update({ + where: { id }, + data: { + status: ProjectStatus.created, + lastSuccessProvisionningVersion: appVersion, + }, + include: baseProjectIncludes, + }) +} + +export function updateProjectFailed(id: Project['id']) { + return prisma.project.update({ + where: { id }, + data: { status: ProjectStatus.failed }, + include: baseProjectIncludes, + }) +} + +export function updateProjectWarning(id: Project['id']) { + return prisma.project.update({ + where: { id }, + data: { status: ProjectStatus.warning }, + include: baseProjectIncludes, + }) +} + +export function addUserToProject({ project, user }: { project: Project, user: User }) { + return prisma.projectMembers.create({ + data: { + userId: user.id, + projectId: project.id, + }, + }) +} + +export function removeUserFromProject({ projectId, userId }: { projectId: Project['id'], userId: User['id'] }) { + return prisma.projectMembers.delete({ + where: { + projectId_userId: { + projectId, + userId, + }, + }, + }) +} + +export async function archiveProject(id: Project['id']) { + const project = await prisma.project.findUnique({ + where: { id }, + select: { name: true, slug: true }, + }) + return prisma.project.update({ + where: { id }, + data: { + name: `${project?.name}_${Date.now()}_archived`, + slug: `${project?.slug}_${Date.now()}_archived`, + status: ProjectStatus.archived, + locked: true, + }, + include: baseProjectIncludes, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts new file mode 100644 index 000000000..bee5f5235 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts @@ -0,0 +1,440 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ProjectV2 } from '@cpn-console/shared' +import { PROJECT_PERMS, projectContract } from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getProjectMockInfos, getRandomRequestor, getUserMockInfos } from '../../utils/mocks.js' +import { BadRequest400 } from '../../utils/errors.js' +import * as business from './business.js' +import type { UserDetails } from '../../types/index.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMock = vi.spyOn(business, 'listProjects') +const businessCreateMock = vi.spyOn(business, 'createProject') +const businessUpdateMock = vi.spyOn(business, 'updateProject') +const businessDeleteMock = vi.spyOn(business, 'archiveProject') +const businessSyncMock = vi.spyOn(business, 'replayHooks') +const bulkActionProjectMock = vi.spyOn(business, 'bulkActionProject') +const businessGetSecretsMock = vi.spyOn(business, 'getProjectSecrets') +const businessGenerateDataMock = vi.spyOn(business, 'generateProjectsData') + +describe('test projectContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + const projectOwner: ProjectV2['owner'] = { + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + id: faker.string.uuid(), + type: 'human', + } + const projectId = faker.string.uuid() + const project: Omit = { + name: faker.string.alpha({ length: 10, casing: 'lower' }), + slug: faker.string.alpha({ length: 5, casing: 'lower' }), + description: faker.string.alpha({ length: 5 }), + limitless: false, + hprodCpu: faker.number.int({ min: 0, max: 1000 }), + hprodGpu: faker.number.int({ min: 0, max: 1000 }), + hprodMemory: faker.number.int({ min: 0, max: 1000 }), + prodCpu: faker.number.int({ min: 0, max: 1000 }), + prodGpu: faker.number.int({ min: 0, max: 1000 }), + prodMemory: faker.number.int({ min: 0, max: 1000 }), + clusterIds: [], + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + locked: false, + status: 'created', + everyonePerms: '0', + members: [], + owner: projectOwner, + ownerId: projectOwner.id, + roles: [], + lastSuccessProvisionningVersion: null, + } + describe('check unauthorized user on project behaviour', () => { + // UPDATE + it('on Update', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(project) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(404) + expect(response.json()).toEqual({ message: 'Not Found' }) + }) + + it('on Update without enough perms', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(project) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Forbidden' }) + }) + + // REPLAY + it('on replay', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) + .end() + + expect(businessSyncMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(404) + expect(response.json()).toEqual({ message: 'Not Found' }) + }) + + // SECRETS + it('on see secret', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) + .end() + + expect(businessGetSecretsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(404) + expect(response.json()).toEqual({ message: 'Not Found' }) + }) + + // ARCHIVE + it('on archive', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectContract.archiveProject.path.replace(':projectId', projectId)) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(404) + expect(response.json()).toEqual({ message: 'Not Found' }) + }) + }) + describe('listProjects', () => { + it('should return list of projects', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + const projects = [] + businessListMock.mockResolvedValueOnce(projects) + const response = await app.inject() + .get(projectContract.listProjects.path) + .end() + + expect(businessListMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(projects) + expect(response.statusCode).toEqual(200) + }) + it('should return 400 for non-admin with "all" filter', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + const response = await app.inject() + .get(`${projectContract.listProjects.path}?filter=all`) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('createProject', () => { + it('should create and return project for authorized user', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce({ id: projectId, ...project }) + const response = await app.inject() + .post(projectContract.createProject.path) + .body(project) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ id: projectId, ...project }) + expect(response.statusCode).toEqual(201) + }) + + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(projectContract.createProject.path) + .body(project) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('updateProject', () => { + const projectUpdated: Partial = { description: faker.string.alpha({ length: 5 }) } + + it('should update and return project for authorized user', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(projectUpdated) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) + expect(response.statusCode).toEqual(200) + }) + + it('should not update ownerId if not permitted', async () => { + const userDetails = getRandomRequestor() + const projectPerms = getProjectMockInfos({ projectOwnerId: faker.string.uuid(), projectPermissions: PROJECT_PERMS.MANAGE }) + const projectUpdated = { ownerId: faker.string.uuid(), description: faker.lorem.words() } + const user = getUserMockInfos(false, userDetails as UserDetails, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(projectUpdated) + .end() + + expect(businessUpdateMock).toHaveBeenCalledWith({ description: projectUpdated.description }, projectId, user.user, expect.any(String)) + expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) + expect(response.statusCode).toEqual(200) + }) + + it('should update ownerId and return project', async () => { + const requestor = getRandomRequestor() + const projectPerms = getProjectMockInfos({ projectOwnerId: requestor.id, projectPermissions: PROJECT_PERMS.MANAGE }) + const projectUpdated = { ownerId: faker.string.uuid(), description: faker.lorem.words() } + const user = getUserMockInfos(false, requestor as UserDetails, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(projectUpdated) + .end() + + expect(businessUpdateMock).toHaveBeenCalledWith(projectUpdated, projectId, user.user, expect.any(String)) + expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) + expect(response.statusCode).toEqual(200) + }) + + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(project) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(1) + expect(response.statusCode).toEqual(400) + }) + }) + + describe('archiveProject', () => { + it('should archive project for authorized user', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(1) + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return projects data for admin', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getProjectSecrets', () => { + it('should return project secrets for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const secrets = {} + businessGetSecretsMock.mockResolvedValueOnce(secrets) + const response = await app.inject() + .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) + .end() + + expect(businessGetSecretsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(secrets) + expect(response.statusCode).toEqual(200) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetSecretsMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for unauthorized access to secrets', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('replayHooksForProject', () => { + it('should replay hooks for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(null) + const response = await app.inject() + .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) + .end() + + expect(businessSyncMock).toHaveBeenCalledTimes(1) + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for unauthorized access to replay hooks', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + const response = await app.inject() + .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getProjectsData', () => { + it('should return projects data for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + const data = '' + businessGenerateDataMock.mockResolvedValueOnce(data) + const response = await app.inject() + .get(projectContract.getProjectsData.path) + .end() + + expect(businessGenerateDataMock).toHaveBeenCalledTimes(1) + expect(response.body).toEqual(data) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for non-admin user', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectContract.getProjectsData.path) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('bulkActionProject', () => { + it('should executebulk for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(null) + const response = await app.inject() + .post(projectContract.bulkActionProject.path) + .body({ action: 'lock', projectIds: [projectId] }) + .end() + + expect(response.json()).toBeNull() + expect(bulkActionProjectMock).toHaveBeenCalledTimes(1) + expect(response.statusCode).toEqual(202) + }) + + it('should return 403 for unauthorized access to bulk update', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + const response = await app.inject() + .post(projectContract.bulkActionProject.path) + .body({ action: 'lock', projectIds: [projectId] }) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts new file mode 100644 index 000000000..d08d17f88 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts @@ -0,0 +1,199 @@ +import type { AsyncReturnType } from '@cpn-console/shared' +import { AdminAuthorized, ProjectAuthorized, projectContract } from '@cpn-console/shared' +import { + archiveProject, + bulkActionProject, + createProject, + generateProjectsData, + getProject, + getProjectSecrets, + listProjects, + replayHooks, + updateProject, +} from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@/utils/errors.js' + +export function projectRouter() { + return serverInstance.router(projectContract, { + + // Récupérer des projets + listProjects: async ({ request: req, query }) => { + const { adminPermissions, user } = await authUser(req) + let body: AsyncReturnType = [] + + if (adminPermissions && !user) { // c'est donc un compte de service + query.filter = 'all' + } + if (query.filter === 'all' && !AdminAuthorized.isAdmin(adminPermissions)) { + return new BadRequest400('Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre \'all\'') + } + + body = await listProjects( + query, + user?.id, + ) + + return { + status: 200, + body, + } + }, + + // Récupérer les secrets d'un projet + getProjectSecrets: async ({ request: req, params }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.SeeSecrets(perms)) return new Forbidden403() + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const body = await getProjectSecrets(projectId) + + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + // Créer un projet + createProject: async ({ request: req, body: data }) => { + const perms = await authUser(req) + if (perms.user?.type !== 'human') return new Unauthorized401('Cannot find requestor in database') + const body = await createProject(data, perms.user, req.id) + + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + // Récuperer un seul projet + getProject: async ({ request: req, params }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + if (!perms.projectId) return new NotFound404() + if (!isAdmin) { + if (!perms.projectPermissions) { + return new NotFound404() + } + if (perms.projectStatus === 'archived') { + return new NotFound404() + } + } + + const body = await getProject(projectId) + + return { + status: 200, + body, + } + }, + + // Mettre à jour un projet + updateProject: async ({ request: req, params, body: data }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + + if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + const isOwner = perms.projectOwnerId === perms.user.id + + if (!perms.projectPermissions && !isAdmin) return new NotFound404() + if (!isAdmin) { // filtrage des clés par niveau de permissions + delete data.locked + if (!isOwner) { + delete data.ownerId // impossible de toucher à cette clé + } + } + if (perms.projectLocked) { + if (!isAdmin) return new Forbidden403('Le projet est verrouillé') + if (data.locked !== false) return new Forbidden403('Veuillez déverrouiler le projet pour le mettre à jour') + } + + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() + + const body = await updateProject(data, projectId, perms.user, req.id) + + if (body instanceof ErrorResType) return body + return { + status: 200, + body, + } + }, + + // Reprovisionner un projet + replayHooksForProject: async ({ request: req, params }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + if (!perms.projectPermissions && !isAdmin) return new NotFound404() + if (!ProjectAuthorized.ReplayHooks(perms)) return new Forbidden403() + + const body = await replayHooks({ + projectId, + userId: perms.user?.id, + requestId: req.id, + }) + + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + + // Archiver un projet + archiveProject: async ({ request: req, params }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + if (!perms.projectPermissions && !isAdmin) return new NotFound404() + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() + + const body = await archiveProject(projectId, perms.user, req.id) + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + // Récupérer les données de tous les projets pour export + getProjectsData: async ({ request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + const body = await generateProjectsData() + + return { + status: 200, + body, + } + }, + + bulkActionProject: async ({ request: req, body }) => { + const perms = await authUser(req) + + if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + await bulkActionProject(body, perms.user, req.id) + + return { + status: 202, + body: null, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts new file mode 100644 index 000000000..4f0e11716 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts @@ -0,0 +1,14 @@ +export * from '@/resources/admin-role/queries.js' +export * from '@/resources/cluster/queries.js' +export * from '@/resources/service-chain/queries.js' +export * from '@/resources/environment/queries.js' +export * from '@/resources/log/queries.js' +export * from '@/resources/project/queries.js' +export * from '@/resources/project-member/queries.js' +export * from '@/resources/project-role/queries.js' +export * from '@/resources/project-service/queries.js' +export * from '@/resources/repository/queries.js' +export * from '@/resources/user/queries.js' +export * from '@/resources/stage/queries.js' +export * from '@/resources/zone/queries.js' +export * from '@/resources/system/settings/queries.js' diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts new file mode 100644 index 000000000..378990d3a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts @@ -0,0 +1,115 @@ +import type { Project, Repository, User } from '@prisma/client' +import type { CreateRepositoryBody, UpdateRepositoryBody } from '@cpn-console/shared' +import { addLogs, deleteRepository as deleteRepositoryQuery, getProjectInfosAndRepos, getProjectRepositories as getProjectRepositoriesQuery, initializeRepository, updateRepository as updateRepositoryQuery } from '@/resources/queries-index.js' +import { BadRequest400, Unprocessable422 } from '@/utils/errors.js' +import { hook } from '@/utils/hook-wrapper.js' + +export async function getProjectRepositories(projectId: Project['id']) { + return getProjectRepositoriesQuery(projectId) +} + +export async function syncRepository({ + repositoryId, + userId, + syncAllBranches, + branchName, + requestId, +}: { + repositoryId: Repository['id'] + userId: User['id'] + syncAllBranches: boolean + branchName?: string + requestId: string +}) { + const hookReply = await hook.misc.syncRepository(repositoryId, { syncAllBranches, branchName }) + await addLogs({ action: 'Sync Repository', data: hookReply, userId, requestId, projectId: hookReply.args.id }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la synchronisation du dépôt') + } + return null +} + +export async function createRepository({ + data, + userId, + requestId, +}: { + data: CreateRepositoryBody + userId: User['id'] + requestId: string +}) { + const project = await getProjectInfosAndRepos(data.projectId) + + if (project.repositories?.find(repo => repo.internalRepoName === data.internalRepoName)) return new BadRequest400(`Le nom du dépôt interne ${data.internalRepoName} existe déjà en base pour ce projet`) + const dbData = { ...data, isInfra: !!data.isInfra, isPrivate: !!data.isPrivate } + delete dbData.externalToken + + const repo = await initializeRepository(dbData) + const { results } = await hook.project.upsert(project.id, data.isPrivate + ? { + [repo.internalRepoName]: { + token: data.externalToken ?? '', + username: data.externalUserName ?? '', + }, + } + : undefined) + await addLogs({ action: 'Create Repository', data: results, userId, requestId, projectId: repo.projectId }) + if (results.failed) { + return new Unprocessable422('Echec des services lors de la création du dépôt') + } + + if (data.externalRepoUrl) { + await syncRepository({ repositoryId: repo.id, requestId, syncAllBranches: true, userId }) + } + return repo +} + +export async function updateRepository({ + repositoryId, + data, + userId, + requestId, +}: { + repositoryId: Repository['id'] + data: Partial + userId: User['id'] + requestId: string +}) { + const dbData = { ...data } + delete dbData.externalToken + const repo = await updateRepositoryQuery(repositoryId, dbData) + + const { results } = await hook.project.upsert(repo.projectId, { + [repo.internalRepoName]: { + username: repo.externalUserName ?? '', + token: data.externalToken ?? '', + }, + }) + await addLogs({ action: 'Update Repository', data: results, userId, requestId, projectId: repo.projectId }) + if (results.failed) { + return new Unprocessable422('Echec des services à la mise à jour du dépôt') + } + + return repo +} + +export async function deleteRepository({ + repositoryId, + userId, + requestId, + projectId, +}: { + repositoryId: Repository['id'] + userId: User['id'] + requestId: string + projectId: Project['id'] +}) { + await deleteRepositoryQuery(repositoryId) + + const { results } = await hook.project.upsert(projectId) + await addLogs({ action: 'Delete Repository', data: results, userId, requestId, projectId }) + if (results.failed) { + return new Unprocessable422('Echec des services à la suppression du dépôt') + } + return null +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts new file mode 100644 index 000000000..48992cd66 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts @@ -0,0 +1,59 @@ +import type { Project, Repository } from '@prisma/client' +import prisma from '@/prisma.js' + +// SELECT +export function getRepositoryById(id: Repository['id']) { + return prisma.repository.findUniqueOrThrow({ where: { id } }) +} + +export function getProjectRepositories(projectId: Project['id']) { + return prisma.repository.findMany({ where: { projectId } }) +} + +// CREATE +type RepositoryCreate = Pick & + Partial> + +export function initializeRepository({ projectId, internalRepoName, externalRepoUrl, isInfra, isPrivate, externalUserName }: RepositoryCreate) { + return prisma.repository.create({ + data: { + projectId, + internalRepoName, + externalRepoUrl, + externalUserName, + isInfra, + isPrivate, + }, + }) +} + +export function getHookRepository(id: Repository['id']) { + return prisma.repository.findUniqueOrThrow({ + where: { + id, + }, + include: { + project: true, + }, + }) +} + +// UPDATE +export function updateRepository(id: Repository['id'], infos: Partial) { + return prisma.repository.update({ where: { id }, data: { ...infos } }) +} + +// DELETE +export async function deleteRepository(id: Repository['id']) { + const doesRepoExist = await getRepositoryById(id) + if (!doesRepoExist) throw new Error('Le dépôt interne demandé n\'existe pas en base pour ce projet') + return prisma.repository.delete({ where: { id } }) +} + +export function deleteAllRepositoryForProject(id: Project['id']) { + return prisma.repository.deleteMany({ where: { projectId: id } }) +} + +export function _createRepository(data: Parameters[0]['create']) { + return prisma.repository.upsert({ create: data, update: data, where: { id: data.id } }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts new file mode 100644 index 000000000..a08f384e3 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts @@ -0,0 +1,402 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PROJECT_PERMS, repositoryContract } from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { atDates, getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' +import { BadRequest400 } from '../../utils/errors.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessCreateMock = vi.spyOn(business, 'createRepository') +const businessUpdateMock = vi.spyOn(business, 'updateRepository') +const businessDeleteMock = vi.spyOn(business, 'deleteRepository') +const businessSyncMock = vi.spyOn(business, 'syncRepository') +const businessGetProjectRepositoriesMock = vi.spyOn(business, 'getProjectRepositories') + +describe('repositoryRouter tests', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + const projectId = faker.string.uuid() + const repositoryId = faker.string.uuid() + const repositoryData = { + projectId, + externalRepoUrl: `${faker.internet.url()}.git`, + isPrivate: true, + externalToken: faker.string.alpha(), + externalUserName: faker.internet.username(), + isInfra: false, + internalRepoName: faker.string.alpha({ length: 5, casing: 'lower' }), + } + + describe('listRepositories', () => { + it('should return repositories for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetProjectRepositoriesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(repositoryContract.listRepositories.path) + .query({ projectId }) + .end() + + expect(businessGetProjectRepositoriesMock).toHaveBeenCalledWith(projectId) + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should return empty for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.REPLAY_HOOKS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(repositoryContract.listRepositories.path) + .query({ projectId }) + .end() + + expect(businessGetProjectRepositoriesMock).toHaveBeenCalledTimes(0) + expect(response.json()).toEqual([]) + }) + }) + + describe('syncRepository', () => { + it('should synchronize repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(204) + expect(businessSyncMock).toHaveBeenCalledWith({ repositoryId, userId: user.user.id, branchName: 'main', requestId: expect.any(String), syncAllBranches: false }) + }) + + it('should return 403 for forbidden sync attempt', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('createRepository', () => { + it('should create repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...atDates }) + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(201) + expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData }) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(404) + }) + it('should return 403 for insuficient permissions', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(403) + }) + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('updateRepository', () => { + const repoUpdateData = { isInfra: true } + it('should update repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...repoUpdateData, ...atDates }) + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(200) + expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData, ...repoUpdateData }) + }) + + it('should update repository and drop creds if is not private', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const repoUpdateData = { isPrivate: false, externalUserName: 'test' } + businessUpdateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...repoUpdateData, ...atDates }) + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(businessUpdateMock).toHaveBeenCalledWith({ data: { isPrivate: false }, repositoryId, requestId: expect.any(String), userId: user.user.id }) + expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData, ...repoUpdateData }) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not enough permissions', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(400) + }) + // TODO add tests about filtering + }) + + describe('deleteRepository', () => { + it('should delete repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + it('should return 403 if not enough privilege', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts new file mode 100644 index 000000000..42e0e2b4d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts @@ -0,0 +1,135 @@ +import { AdminAuthorized, ProjectAuthorized, fakeToken, repositoryContract } from '@cpn-console/shared' +import { + createRepository, + deleteRepository, + getProjectRepositories, + syncRepository, + updateRepository, +} from './business.js' +import { serverInstance } from '@/app.js' + +import { filterObjectByKeys } from '@/utils/queries-tools.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@/utils/errors.js' + +export function repositoryRouter() { + return serverInstance.router(repositoryContract, { + // Récupérer tous les repositories d'un projet + listRepositories: async ({ request: req, query }) => { + const projectId = query.projectId + const perms = await authUser(req, { id: projectId }) + + const body = ProjectAuthorized.ListRepositories(perms) + ? await getProjectRepositories(projectId) + : [] + + return { + status: 200, + body, + } + }, + + // Synchroniser un repository + syncRepository: async ({ request: req, params, body }) => { + const { repositoryId } = params + const perms = await authUser(req, { repositoryId }) + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const { syncAllBranches, branchName } = body + + const resBody = await syncRepository({ repositoryId, userId: perms.user.id, branchName, requestId: req.id, syncAllBranches }) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 204, + body: resBody, + } + }, + + // Créer un repository + createRepository: async ({ request: req, body: data }) => { + const projectId = data.projectId + const perms = await authUser(req, { id: projectId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const body = await createRepository({ data, userId: perms.user.id, requestId: req.id }) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + // Mettre à jour un repository + updateRepository: async ({ request: req, params, body }) => { + const repositoryId = params.repositoryId + const perms = await authUser(req, { repositoryId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const keysAllowedForUpdate = [ + 'externalRepoUrl', + 'isPrivate', + 'externalToken', + 'externalUserName', + 'isInfra', + ] + const data = filterObjectByKeys(body, keysAllowedForUpdate) + + if (data.externalToken === fakeToken) { + delete data.externalToken + } + + if (data.isPrivate === false) { + delete data.externalToken + delete data.externalUserName + } + + const resBody = await updateRepository({ repositoryId, data, userId: perms.user.id, requestId: req.id }) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 200, + body: resBody, + } + }, + + // Supprimer un repository + deleteRepository: async ({ request: req, params }) => { + const repositoryId = params.repositoryId + const perms = await authUser(req, { repositoryId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const body = await deleteRepository({ + repositoryId, + userId: perms.user.id, + requestId: req.id, + projectId: perms.projectId, + }) + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts new file mode 100644 index 000000000..7e082d817 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts @@ -0,0 +1,171 @@ +import type { + ServiceChain, + ServiceChainDetails, + ServiceChainFlows, +} from '@cpn-console/shared' +import { + serviceChainEnvironmentEnum, + serviceChainFlowStateEnum, + serviceChainLocationEnum, + serviceChainNetworkEnum, + serviceChainStateEnum, +} from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import axios from 'axios' +import type { Mock } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { + getServiceChainDetails, + getServiceChainFlows, + listServiceChains, + retryServiceChain, + validateServiceChain, +} from './business.ts' + +vi.mock('axios') + +let serviceChain: ServiceChain +let serviceChainDetails: ServiceChainDetails +let serviceChainFlows: ServiceChainFlows + +describe('test ServiceChain business logic', () => { + beforeEach(() => { + serviceChain = { + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement(serviceChainNetworkEnum), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + } + + serviceChainDetails = { + ...serviceChain, + validationId: faker.string.uuid(), + validatedBy: faker.helpers.maybe(() => faker.string.uuid()) || null, + ref: faker.string.uuid(), + location: faker.helpers.arrayElement(serviceChainLocationEnum), + targetAddress: faker.internet.ipv4(), + projectId: faker.string.uuid(), + env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), + subjectAlternativeName: faker.helpers.uniqueArray( + faker.internet.domainName, + 3, + ), + redirect: faker.datatype.boolean(), + antivirus: + faker.helpers.maybe(() => ({ + maxFileSize: faker.number.int(), + })) || null, // undefined is not wanted here + websocket: faker.datatype.boolean(), + ipWhiteList: faker.helpers + .uniqueArray(faker.internet.ipv4, 5) + .map(e => `${e}/32`), // We want a CIDR here + sslOutgoing: faker.datatype.boolean(), + } + + serviceChainFlows = { + reserve_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + create_cert: faker.helpers.maybe(() => ({ + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + })) || null, + call_exec: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + activate_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + dns_request: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + } + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('listServiceChains', () => { + it('should return a list of service chains', async () => { + const input = [serviceChain]; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }) + + const result = await listServiceChains() + + expect(result).toStrictEqual(input) + }) + }) + + describe('getServiceChainDetails', () => { + it('should return a service chain details', async () => { + const input = serviceChainDetails; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }) + + const result = await getServiceChainDetails(faker.string.uuid()) + + expect(result).toStrictEqual(input) + }) + }) + + describe('retryServiceChain', () => { + it('should trigger a service chain retry attempt', async () => { + const input = {}; + (axios.create as Mock).mockReturnValue({ + post: () => ({ data: input }), + }) + + const result = await retryServiceChain(faker.string.uuid()) + + expect(result.data).toStrictEqual(input) + }) + }) + + describe('validateServiceChain', () => { + it('should trigger a service chain validate attempt', async () => { + const input = {}; + (axios.create as Mock).mockReturnValue({ + post: () => ({ data: input }), + }) + + const result = await validateServiceChain(faker.string.uuid()) + + expect(result.data).toStrictEqual(input) + }) + }) + + describe('getServiceChainFlows', () => { + it('should return a service chain flows', async () => { + const input = serviceChainFlows; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }) + + const result = await getServiceChainFlows(faker.string.uuid()) + + expect(result).toStrictEqual(input) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts new file mode 100644 index 000000000..c245279d4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts @@ -0,0 +1,27 @@ +import { + getServiceChainDetails as getServiceChainDetailsQuery, + listServiceChains as listServiceChainsQuery, + retryServiceChain as retryServiceChainQuery, + validateServiceChain as validateServiceChainQuery, + getServiceChainFlows as getServiceChainFlowsQuery, +} from '@/resources/queries-index.js' + +export async function listServiceChains() { + return listServiceChainsQuery() +} + +export async function getServiceChainDetails(serviceChainId: string) { + return getServiceChainDetailsQuery(serviceChainId) +} + +export async function retryServiceChain(serviceChainId: string) { + return retryServiceChainQuery(serviceChainId) +} + +export async function validateServiceChain(validationId: string) { + return validateServiceChainQuery(validationId) +} + +export async function getServiceChainFlows(serviceChainId: string) { + return getServiceChainFlowsQuery(serviceChainId) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts new file mode 100644 index 000000000..10713007c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts @@ -0,0 +1,58 @@ +import { + type ServiceChain, + ServiceChainDetailsSchema, + ServiceChainFlowsSchema, + ServiceChainListSchema, +} from '@cpn-console/shared' +import axios from 'axios' +import https from 'node:https' + +const openCDSEnvVar = 'OPENCDS_URL' +const openCDSTargetURL = process.env[openCDSEnvVar] +const openCDSDisabledErrorMessage = `OpenCDS is disabled, please set ${openCDSEnvVar} in your relevant .env file. See .env-example` + +function getClient() { + if (!openCDSTargetURL) { + throw new Error(openCDSDisabledErrorMessage) + } + return axios.create({ + baseURL: openCDSTargetURL, + httpsAgent: new https.Agent({ + rejectUnauthorized: + // We want it to be `false` only if it has explicitly + // been stated as "false" in the env vars + process.env.OPENCDS_API_TLS_REJECT_UNAUTHORIZED !== 'false', + }), + headers: { + 'X-API-Key': process.env.OPENCDS_API_TOKEN, + }, + }) +} + +export async function listServiceChains() { + return ServiceChainListSchema.parse( + (await getClient().get(`/requests`)).data, + ) +} + +export async function getServiceChainDetails( + serviceChainId: ServiceChain['id'], +) { + return ServiceChainDetailsSchema.parse( + (await getClient().get(`/requests/${serviceChainId}`)).data, + ) +} + +export async function retryServiceChain(serviceChainId: ServiceChain['id']) { + return await getClient().post(`/requests/${serviceChainId}/retry`) +} + +export async function validateServiceChain(validationId: string) { + return await getClient().post(`/validate/${validationId}`) +} + +export async function getServiceChainFlows(serviceChainId: ServiceChain['id']) { + return ServiceChainFlowsSchema.parse( + (await getClient().get(`/requests/${serviceChainId}/flows`)).data, + ) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts new file mode 100644 index 000000000..4edf4438f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts @@ -0,0 +1,306 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ServiceChain, ServiceChainDetails, ServiceChainFlows } from '@cpn-console/shared' +import { + ServiceChainDetailsSchema, + ServiceChainFlowsSchema, + ServiceChainListSchema, + serviceChainContract, + serviceChainEnvironmentEnum, + serviceChainFlowStateEnum, + serviceChainLocationEnum, + serviceChainNetworkEnum, + serviceChainStateEnum, +} from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getUserMockInfos } from '../../utils/mocks.js' +import * as business from './business.js' + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListServiceChainsMock = vi.spyOn(business, 'listServiceChains') +const businessGetServiceChainDetailsMock = vi.spyOn(business, 'getServiceChainDetails') +const businessRetryServiceChainMock = vi.spyOn(business, 'retryServiceChain') +const businessValidateServiceChainMock = vi.spyOn(business, 'validateServiceChain') +const businessGetServiceChainsFlowsMock = vi.spyOn(business, 'getServiceChainFlows') + +describe('test ServiceChainContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + describe('listServiceChains', () => { + it('as non admin', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + businessListServiceChainsMock.mockResolvedValueOnce([]) + const response = await app + .inject() + .get(serviceChainContract.listServiceChains.path) + .end() + + expect(response.json()).toStrictEqual([]) + expect(response.statusCode).toEqual(200) + }) + it('as admin', async () => { + const user = getUserMockInfos(true) + const serviceChainList = faker.helpers.multiple(() => ({ + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement(serviceChainNetworkEnum), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + })) + + authUserMock.mockResolvedValueOnce(user) + + businessListServiceChainsMock.mockResolvedValueOnce(serviceChainList) + const response = await app + .inject() + .get(serviceChainContract.listServiceChains.path) + .end() + + expect(businessListServiceChainsMock).toHaveBeenCalledWith() + + expect(ServiceChainListSchema.parse(response.json())).toStrictEqual( + serviceChainList, + ) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('getServiceChainDetails', () => { + it('should return serviceChain details', async () => { + const serviceChainDetails: ServiceChainDetails = { + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement(serviceChainNetworkEnum), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + validationId: faker.string.uuid(), + validatedBy: faker.string.uuid(), + ref: faker.string.uuid(), + location: faker.helpers.arrayElement(serviceChainLocationEnum), + targetAddress: faker.internet.ipv4(), + projectId: faker.string.uuid(), + env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), + subjectAlternativeName: faker.helpers.uniqueArray( + faker.internet.domainName, + 3, + ), + redirect: faker.datatype.boolean(), + antivirus: + faker.helpers.maybe(() => ({ + maxFileSize: faker.number.int(), + })) || null, // undefined is not wanted here + websocket: faker.datatype.boolean(), + ipWhiteList: faker.helpers + .uniqueArray(faker.internet.ipv4, 5) + .map(e => `${e}/32`), // We want a CIDR here + sslOutgoing: faker.datatype.boolean(), + } + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetServiceChainDetailsMock.mockResolvedValueOnce(serviceChainDetails) + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainDetails.path.replace( + ':serviceChainId', + serviceChainDetails.id, + ), + ) + .end() + + expect(ServiceChainDetailsSchema.parse(response.json())).toEqual( + serviceChainDetails, + ) + expect(response.statusCode).toEqual(200) + expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(1) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainDetails.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() + + expect(response.statusCode).toEqual(403) + expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(0) + }) + }) + + describe('retryServiceChain', () => { + it('should return 204', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessRetryServiceChainMock.mockResolvedValueOnce({ + status: 204, + body: undefined, + }) + const response = await app + .inject() + .post( + serviceChainContract.retryServiceChain.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() + + expect(response.body).toEqual('') + expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(1) + expect(response.statusCode).toEqual(204) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app + .inject() + .post( + serviceChainContract.retryServiceChain.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() + + expect(response.statusCode).toEqual(403) + expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(0) + }) + }) + + describe('validateServiceChain', () => { + it('should return 204', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessValidateServiceChainMock.mockResolvedValueOnce({ + status: 204, + body: undefined, + }) + const response = await app + .inject() + .post( + serviceChainContract.validateServiceChain.path.replace( + ':validationId', + faker.string.uuid(), + ), + ) + .end() + + expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(1) + expect(response.body).toEqual('') + expect(response.statusCode).toEqual(204) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app + .inject() + .post( + serviceChainContract.validateServiceChain.path.replace( + ':validationId', + faker.string.uuid(), + ), + ) + .end() + + expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getServiceChainFlows', () => { + it('should return serviceChain flows', async () => { + const serviceChainFlows: ServiceChainFlows = { + reserve_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + create_cert: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + call_exec: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + activate_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + dns_request: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + } + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetServiceChainsFlowsMock.mockResolvedValueOnce(serviceChainFlows) + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainFlows.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() + + expect(ServiceChainFlowsSchema.parse(response.json())).toEqual( + serviceChainFlows, + ) + expect(response.statusCode).toEqual(200) + expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(1) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainFlows.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() + + expect(response.statusCode).toEqual(403) + expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts new file mode 100644 index 000000000..0f53f3c16 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts @@ -0,0 +1,90 @@ +import type { AsyncReturnType } from '@cpn-console/shared' +import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared' +import { + listServiceChains as listServiceChainsBusiness, + getServiceChainDetails as getServiceChainDetailsBusiness, + retryServiceChain as retryServiceChainBusiness, + validateServiceChain as validateServiceChainBusiness, + getServiceChainFlows as getServiceChainFlowsBusiness, +} from './business.js' +import '@/types/index.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { Forbidden403 } from '@/utils/errors.js' + +export function serviceChainRouter() { + return serverInstance.router(serviceChainContract, { + listServiceChains: async ({ request: req }) => { + const { adminPermissions } = await authUser(req) + + let body: AsyncReturnType = [] + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listServiceChainsBusiness() + } + + return { + status: 200, + body, + } + }, + + getServiceChainDetails: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403() + + const serviceChainId = params.serviceChainId + const serviceChainDetails + = await getServiceChainDetailsBusiness(serviceChainId) + + return { + status: 200, + body: serviceChainDetails, + } + }, + + retryServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403() + + const serviceChainId = params.serviceChainId + await retryServiceChainBusiness(serviceChainId) + + return { + status: 204, + body: null, + } + }, + + validateServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403() + + const serviceChainId = params.validationId + await validateServiceChainBusiness(serviceChainId) + + return { + status: 204, + body: null, + } + }, + + getServiceChainFlows: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403() + + const serviceChainId = params.serviceChainId + const serviceChainFlows + = await getServiceChainFlowsBusiness(serviceChainId) + + return { + status: 200, + body: serviceChainFlows, + } + }, + + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts new file mode 100644 index 000000000..fa61d5a6d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts @@ -0,0 +1,9 @@ +import { services } from '@cpn-console/hooks' + +export function checkServicesHealth() { + return services.getStatus() +} + +export async function refreshServicesHealth() { + return Promise.all(services.refreshStatus()) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts new file mode 100644 index 000000000..1ec2528fc --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts @@ -0,0 +1,78 @@ +import { describe, expect, it, vi } from 'vitest' +import { MonitorStatus, serviceContract } from '@cpn-console/shared' +import type { ServiceStatus } from '@cpn-console/hooks' +import app from '../../app.js' +import * as business from './business.js' +import { getUserMockInfos } from '../../utils/mocks.js' +import * as utilsController from '../../utils/controller.js' + +const authUserMock = vi.spyOn(utilsController, 'authUser') + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const businessCheckMock = vi.spyOn(business, 'checkServicesHealth') +const businessRefreshMock = vi.spyOn(business, 'refreshServicesHealth') + +describe('test serviceContract', () => { + const services: ServiceStatus[] = [{ interval: 1, lastUpdateTimestamp: 1, message: 'OK', name: 'A service', status: MonitorStatus.OK }] + const servicesComplete: ServiceStatus[] = [{ cause: 'error', interval: 1, lastUpdateTimestamp: 1, message: 'OK', name: 'A service', status: MonitorStatus.OK }] + + it('should return complete services, with cause', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessCheckMock.mockReturnValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end() + + expect(response.json()).toStrictEqual(servicesComplete) + expect(response.statusCode).toEqual(200) + }) + + it('should not return complete services, forbidden', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + businessCheckMock.mockReturnValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return services', async () => { + businessCheckMock.mockReturnValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getServiceHealth.path) + .end() + + expect(response.json()).toStrictEqual(services) + expect(response.statusCode).toEqual(200) + }) + + it('should refresh services', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessRefreshMock.mockResolvedValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end() + + expect(response.json()).toStrictEqual(servicesComplete) + expect(response.statusCode).toEqual(200) + }) + + it('should refresh services, cause forbidden', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + businessRefreshMock.mockResolvedValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end() + + expect(response.statusCode).toEqual(403) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts new file mode 100644 index 000000000..a12fea66e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts @@ -0,0 +1,43 @@ +import { AdminAuthorized, serviceContract } from '@cpn-console/shared' +import { checkServicesHealth, refreshServicesHealth } from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { Forbidden403 } from '@/utils/errors.js' + +export function serviceMonitorRouter() { + return serverInstance.router(serviceContract, { + getServiceHealth: async () => { + const serviceData = checkServicesHealth() + + return { + status: 200, + body: serviceData, + } + }, + + getCompleteServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req) + + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + const serviceData = checkServicesHealth() + + return { + status: 200, + body: serviceData, + } + }, + + refreshServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + + await refreshServicesHealth() + const serviceData = checkServicesHealth() + + return { + status: 200, + body: serviceData, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts new file mode 100644 index 000000000..d608ee0c0 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts @@ -0,0 +1,113 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Environment, Stage } from '@prisma/client' +import prisma from '../../__mocks__/prisma.js' +import { BadRequest400, NotFound404 } from '../../utils/errors.ts' +import { createStage, deleteStage, getStageAssociatedEnvironments, listStages, updateStage } from './business.ts' + +describe('test stage busines logic', () => { + let stage: Stage + beforeEach(() => { + vi.resetAllMocks() + stage = { + id: faker.string.uuid(), + name: faker.company.name(), + } + }) + describe('createStage', () => { + it('should create a stage', async () => { + prisma.stage.findUnique.mockResolvedValue(null) + prisma.stage.create.mockResolvedValue({ id: stage.id } as Stage) + await createStage({ name: stage.name, clusterIds: [faker.string.uuid()] }) + expect(prisma.stage.update).toHaveBeenCalledTimes(1) + }) + it('should not create a stage, name conflict', async () => { + prisma.stage.findUnique.mockResolvedValue({ id: stage.id } as Stage) + const response = await createStage({ name: stage.name, clusterIds: [faker.string.uuid()] }) + expect(prisma.stage.update).toHaveBeenCalledTimes(0) + expect(response).instanceOf(BadRequest400) + }) + }) + + describe('updateStage', () => { + it('should update a stage', async () => { + const dbClusters = [{ id: faker.string.uuid() }] + const newClusters = [faker.string.uuid()] + prisma.stage.findUnique.mockResolvedValue({ ...stage, clusters: dbClusters } as Stage) + prisma.stage.update.mockResolvedValue({ id: stage.id } as Stage) + const response = await updateStage(stage.id, { name: stage.name, clusterIds: newClusters }) + expect(prisma.cluster.update).toHaveBeenCalledTimes(1) + expect(prisma.cluster.update).toHaveBeenCalledWith({ where: { id: dbClusters[0].id }, data: { + stages: { + disconnect: { + id: stage.id, + }, + }, + } }) + expect(prisma.stage.update).toHaveBeenCalledTimes(1) + expect(prisma.stage.update).toHaveBeenCalledWith({ where: { id: stage.id }, data: { + clusters: { + connect: [{ + id: newClusters[0], + }], + }, + } }) + expect(response.clusterIds).toBe(newClusters) + }) + it('should do nothing', async () => { + prisma.stage.findUnique.mockResolvedValue({ ...stage, clusters: [] } as Stage) + await updateStage(stage.id, { clusterIds: [], name: stage.name }) + expect(prisma.stage.update).toHaveBeenCalledTimes(0) + }) + it('should return not found', async () => { + prisma.stage.findUnique.mockResolvedValue(null) + const response = await updateStage(stage.id, { name: stage.name, clusterIds: [faker.string.uuid()] }) + expect(prisma.stage.update).toHaveBeenCalledTimes(0) + expect(response).instanceOf(NotFound404) + }) + }) + + describe('deleteStage', () => { + it('should delete a stage', async () => { + prisma.environment.findFirst.mockResolvedValue(null) + prisma.stage.delete.mockResolvedValue({ id: stage.id } as Stage) + await deleteStage(stage.id) + expect(prisma.stage.delete).toHaveBeenCalledTimes(1) + }) + it('should not delete a stage, environment attached', async () => { + prisma.environment.findFirst.mockResolvedValue({ id: faker.string.uuid() } as Environment) + const response = await deleteStage(stage.id) + expect(prisma.stage.delete).toHaveBeenCalledTimes(0) + expect(response).instanceOf(BadRequest400) + }) + }) + + describe('listStages', () => { + const clusterAssociated = [{ id: faker.string.uuid() }] + it('should list all stages (admin, no userId provided)', async () => { + prisma.stage.findMany.mockResolvedValue([{ clusters: clusterAssociated }] as unknown as Stage[]) + const response = await listStages() + expect(response[0].clusterIds).toStrictEqual([clusterAssociated[0].id]) + expect(prisma.stage.findMany).toHaveBeenCalledTimes(1) + expect(prisma.stage.findMany).toHaveBeenCalledWith({ include: { clusters: true } }) + }) + }) + + describe('getStageAssociatedEnvironments', () => { + it('should list all environments attached to a stage stages', async () => { + const envName = faker.string.alpha(8) + const projectSlug = faker.string.alpha(8) + const clusterLabel = faker.string.alpha(8) + const ownerEmail = faker.internet.email() + const envs = [{ name: envName, project: { slug: projectSlug, owner: { email: ownerEmail } }, cluster: { label: clusterLabel } }] + prisma.environment.findMany.mockResolvedValue(envs as unknown as Environment[]) + const response = await getStageAssociatedEnvironments(stage.id) + expect(response).toStrictEqual([{ + name: envName, + project: projectSlug, + owner: ownerEmail, + cluster: clusterLabel, + }]) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts new file mode 100644 index 000000000..eb5110ec8 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts @@ -0,0 +1,97 @@ +import type { Cluster, Stage } from '@prisma/client' +import type { CreateStageBody, UpdateStageBody } from '@cpn-console/shared' +import { + createStage as createStageQuery, + deleteStage as deleteStageQuery, + getAllStageIds, + getStageAssociatedEnvironmentById, + getStageById, + getStageByName, + linkClusterToStages as linkClusterToStagesQuery, + linkStageToClusters, + listStages as listStagesQuery, + removeClusterFromStage, + updateStageName, +} from '@/resources/queries-index.js' +import { BadRequest400, NotFound404 } from '@/utils/errors.js' +import prisma from '@/prisma.js' + +export async function getStageAssociatedEnvironments(stageId: Stage['id']) { + const environments = await getStageAssociatedEnvironmentById(stageId) + return environments.map(env => ({ + project: env.project.slug, + name: env.name, + cluster: env.cluster.label, + owner: env.project.owner.email, + })) +} + +export async function createStage({ clusterIds = [], name }: CreateStageBody) { + const isNameTaken = await getStageByName(name) + if (isNameTaken) return new BadRequest400('Un type d\'environnement portant ce nom existe déjà') + + const stage = await createStageQuery({ name }) + + if (clusterIds.length) { + await linkStageToClusters(stage.id, clusterIds) + } + + return { + id: stage.id, + name: stage.name, + clusterIds, + } +} + +export async function updateStage(stageId: Stage['id'], { clusterIds, name }: UpdateStageBody) { + const dbStage = await getStageById(stageId) + if (!dbStage) return new NotFound404() + if (name !== dbStage.name) { + await updateStageName(stageId, name) + } + // Remove clusters + const dbClusters = dbStage.clusters + if (dbClusters?.length) { + const clustersToRemove = dbClusters.filter(dbCluster => !clusterIds.includes(dbCluster.id)) + for (const clusterToRemove of clustersToRemove) { + await removeClusterFromStage(clusterToRemove.id, stageId) + } + } + // Add clusters + if (clusterIds.length) { + await linkStageToClusters(stageId, clusterIds) + } + + return { + id: stageId, + name: name ?? dbStage.name, + clusterIds: clusterIds ?? dbStage.clusters.map(({ id }) => id), + } +} + +export async function deleteStage(stageId: Stage['id']) { + const attachedEnvironment = await prisma.environment.findFirst({ where: { stageId }, select: { id: true } }) + if (attachedEnvironment) return new BadRequest400('Impossible de supprimer le stage, des environnements en activité y ont souscrit') + + await deleteStageQuery(stageId) + return null +} + +export async function listStages() { + const stages = await listStagesQuery() + + return stages.map((stage) => { + return { + id: stage.id, + name: stage.name, + clusterIds: stage.clusters.map(({ id }) => id), + } + }) +} + +export async function linkClusterToStages(clusterId: Cluster['id'], stageIds: Stage['id'][], linkToAll: boolean = false) { + if (linkToAll === true) { + stageIds = await getAllStageIds() + } + await linkClusterToStagesQuery(clusterId, stageIds) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts new file mode 100644 index 000000000..98d526600 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts @@ -0,0 +1,111 @@ +import type { Cluster, Stage } from '@prisma/client' +import prisma from '@/prisma.js' + +export function listStages() { + return prisma.stage.findMany({ + include: { + clusters: true, + }, + }) +} + +export async function getAllStageIds() { + return (await prisma.stage.findMany({ + select: { + id: true, + }, + })).map(({ id }) => id) +} + +export function getStageById(id: Stage['id']) { + return prisma.stage.findUnique({ + where: { id }, + include: { + clusters: true, + }, + }) +} + +export function getStageByIdOrThrow(id: Stage['id']) { + return prisma.stage.findUniqueOrThrow({ + where: { id }, + include: { + clusters: true, + }, + }) +} + +export function getStageAssociatedEnvironmentById(id: Stage['id']) { + return prisma.environment.findMany({ + where: { + stageId: id, + }, + select: { + name: true, + cluster: { + select: { + label: true, + }, + }, + project: { + select: { + name: true, + owner: true, + slug: true, + }, + }, + }, + }) +} + +export function getStageAssociatedEnvironmentLengthById(id: Stage['id']) { + return prisma.environment.count({ + where: { + stageId: id, + }, + }) +} + +export function getStageByName(name: Stage['name']) { + return prisma.stage.findUnique({ + where: { name }, + }) +} + +export function linkStageToClusters(id: Stage['id'], clusterIds: Cluster['id'][]) { + return prisma.stage.update({ + where: { + id, + }, + data: { + clusters: { + connect: clusterIds.map(clusterId => ({ id: clusterId })), + }, + }, + }) +} + +export function createStage({ name }: { name: Stage['name'] }) { + return prisma.stage.create({ + data: { + name, + }, + }) +} + +export function updateStageName(id: Stage['id'], name: Stage['name']) { + return prisma.stage.update({ + where: { + id, + }, + data: { + name, + }, + }) +} + +export function deleteStage(id: Stage['id']) { + return prisma.stage.delete({ + where: { id }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts new file mode 100644 index 000000000..3e1b6293b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts @@ -0,0 +1,202 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Stage } from '@cpn-console/shared' +import { stageContract } from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getUserMockInfos } from '../../utils/mocks.js' +import { BadRequest400 } from '../../utils/errors.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMock = vi.spyOn(business, 'listStages') +const businessGetEnvironmentsMock = vi.spyOn(business, 'getStageAssociatedEnvironments') +const businessCreateMock = vi.spyOn(business, 'createStage') +const businessUpdateMock = vi.spyOn(business, 'updateStage') +const businessDeleteMock = vi.spyOn(business, 'deleteStage') + +describe('test stageContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listStages', () => { + it('should return list of stages', async () => { + const stages = [] + businessListMock.mockResolvedValueOnce(stages) + + const response = await app.inject() + .get(stageContract.listStages.path) + .end() + + expect(businessListMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(stages) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('getStageEnvironments', () => { + it('should return stage environments for admin', async () => { + const environments = [] + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetEnvironmentsMock.mockResolvedValueOnce(environments) + const response = await app.inject() + .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(environments) + expect(response.statusCode).toEqual(200) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetEnvironmentsMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('createStage', () => { + const stage: Stage = { id: faker.string.uuid(), name: faker.string.alpha({ length: 5 }), clusterIds: [] } + + it('should create and return stage for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(stage) + const response = await app.inject() + .post(stageContract.createStage.path) + .body(stage) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(stage) + expect(response.statusCode).toEqual(201) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(stageContract.createStage.path) + .body(stage) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(stageContract.createStage.path) + .body(stage) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('updateStage', () => { + const stageId = faker.string.uuid() + const stage = { name: faker.string.alpha({ length: 5 }), clusterIds: [] } + + it('should update and return stage for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: stageId, ...stage }) + const response = await app.inject() + .put(stageContract.updateStage.path.replace(':stageId', stageId)) + .body(stage) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ id: stageId, ...stage }) + expect(response.statusCode).toEqual(200) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(stageContract.updateStage.path.replace(':stageId', stageId)) + .body(stage) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(stageContract.updateStage.path.replace(':stageId', stageId)) + .body(stage) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('deleteStage', () => { + it('should delete stage for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(1) + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts new file mode 100644 index 000000000..8a2f5f9a5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts @@ -0,0 +1,88 @@ +import { AdminAuthorized, stageContract } from '@cpn-console/shared' +import { + createStage, + deleteStage, + getStageAssociatedEnvironments, + listStages, + updateStage, +} from './business.js' +import { serverInstance } from '@/app.js' + +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@/utils/errors.js' + +export function stageRouter() { + return serverInstance.router(stageContract, { + + // Récupérer les types d'environnement disponibles + listStages: async () => { + const body = await listStages() + + return { + status: 200, + body, + } + }, + + // Récupérer les environnements associés au stage + getStageEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const stageId = params.stageId + const body = await getStageAssociatedEnvironments(stageId) + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + // Créer un stage + createStage: async ({ request: req, body: data }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const body = await createStage(data) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + // Modifier une association stage / clusters + updateStage: async ({ request: req, params, body: data }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const stageId = params.stageId + + const body = await updateStage(stageId, data) + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + // Supprimer un stage + deleteStage: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const stageId = params.stageId + + const body = await deleteStage(stageId) + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts new file mode 100644 index 000000000..35d484407 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import prisma from '../../../__mocks__/prisma.js' +import { objToDb, updatePluginConfig } from './business.ts' + +describe('test system/config business', () => { + const config = { test: { key1: 'value1' } } + it('should transform object to db row', () => { + const response = objToDb({ test: { key1: 'value1' } }) + expect(response).toEqual([{ pluginName: 'test', key: 'key1', value: 'value1' }]) + }) + describe('updatePluginConfig', () => { + it('should update', async () => { + prisma.adminPlugin.upsert.mockResolvedValue(null) + await updatePluginConfig(config) + }) + it('should update 0 items cause missing manifest', async () => { + // @ts-ignore + await updatePluginConfig({ test: { key: 1 } }) + expect(prisma.adminPlugin.upsert).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts new file mode 100644 index 000000000..9b89e9ef0 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts @@ -0,0 +1,50 @@ +import type { + PluginsUpdateBody, +} from '@cpn-console/shared' +import { editStrippers, populatePluginManifests, servicesInfos } from '@cpn-console/hooks' +import { + getAdminPlugin, + savePluginsConfig, +} from './queries.js' +import { BadRequest400 } from '@/utils/errors.js' + +export type ConfigRecords = { + key: string + pluginName: string + value: string +}[] + +export function objToDb(obj: PluginsUpdateBody): ConfigRecords { + return Object.entries(obj) + .map(([pluginName, values]) => Object.entries(values) + .map(([key, value]) => ({ pluginName, key, value }))) + .flat() +} + +export async function getPluginsConfig() { + const globalConfig = await getAdminPlugin() + + return Object.values(servicesInfos).map(({ name, title, imgSrc, description }) => { + const manifest = populatePluginManifests({ + data: { + global: globalConfig, + }, + permissionTarget: 'admin', + pluginName: name, + select: { + global: true, + project: false, + }, + }) + return { imgSrc, title, name, manifest: manifest.global ?? [], description } + }).filter(plugin => plugin.manifest.length > 0) +} + +export async function updatePluginConfig(data: PluginsUpdateBody) { + const parsedData = editStrippers.global.safeParse(data) + if (!parsedData.success) return new BadRequest400(parsedData.error.message) + const records = objToDb(parsedData.data) + + await savePluginsConfig(records) + return null +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts new file mode 100644 index 000000000..69808e506 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts @@ -0,0 +1,28 @@ +import type { ConfigRecords } from './business.js' +import prisma from '@/prisma.js' + +// CONFIG +export const getAdminPlugin = prisma.adminPlugin.findMany + +export async function savePluginsConfig(records: ConfigRecords) { + for (const { pluginName, key, value } of records) { + await prisma.adminPlugin.upsert({ + create: { + pluginName, + key, + value: String(value), + }, + update: { + key, + value: String(value), + pluginName, + }, + where: { + pluginName_key: { + pluginName, + key, + }, + }, + }) + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts new file mode 100644 index 000000000..e1f11a105 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { systemPluginContract } from '@cpn-console/shared' +import app from '../../../app.js' +import * as utilsController from '../../../utils/controller.js' +import { getUserMockInfos } from '../../../utils/mocks.js' +import { BadRequest400 } from '../../../utils/errors.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetPluginsConfigMock = vi.spyOn(business, 'getPluginsConfig') +const businessUpdatePluginConfigMock = vi.spyOn(business, 'updatePluginConfig') + +describe('test systemPluginContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('getPluginsConfig', () => { + it('should return plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true) + const pluginsConfig = [] + + authUserMock.mockResolvedValueOnce(user) + businessGetPluginsConfigMock.mockResolvedValueOnce(pluginsConfig) + + const response = await app.inject() + .get(systemPluginContract.getPluginsConfig.path) + .end() + + expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(pluginsConfig) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(systemPluginContract.getPluginsConfig.path) + .end() + + expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('updatePluginsConfig', () => { + const newConfig = { plugin1: { keyId: 'value' } } + it('should update plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessUpdatePluginConfigMock.mockResolvedValueOnce(newConfig) + + const response = await app.inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end() + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith(newConfig) + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end() + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + + it('should return error if business logic fails', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessUpdatePluginConfigMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + + const response = await app.inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end() + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith(newConfig) + expect(response.statusCode).toEqual(400) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts new file mode 100644 index 000000000..534254e6e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts @@ -0,0 +1,36 @@ +import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared' +import { getPluginsConfig, updatePluginConfig } from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@/utils/errors.js' + +export function pluginConfigRouter() { + return serverInstance.router(systemPluginContract, { + // Récupérer les configurations plugins + getPluginsConfig: async ({ request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const services = await getPluginsConfig() + + return { + status: 200, + body: services, + + } + }, + // Mettre à jour les configurations plugins + updatePluginsConfig: async ({ request: req, body }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await updatePluginConfig(body) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 204, + body: resBody, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts new file mode 100644 index 000000000..a45d7accc --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts @@ -0,0 +1 @@ +export * from './router.js' diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts new file mode 100644 index 000000000..321980ff5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi } from 'vitest' +import { systemContract } from '@cpn-console/shared' +import app from '../../app.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) + +describe('system - router', () => { + it('should send application version', async () => { + const response = await app.inject() + .get(systemContract.getVersion.path) + .end() + + expect(response.statusCode).toBe(200) + expect(response.json()).toStrictEqual({ version: process.env.APP_VERSION || 'dev' }) + }) + + it('should send application health with status OK', async () => { + const response = await app.inject() + .get(systemContract.getHealth.path) + .end() + + expect(response.statusCode).toBe(200) + expect(response.json()).toStrictEqual({ status: 'OK' }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts new file mode 100644 index 000000000..997688496 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts @@ -0,0 +1,21 @@ +import { systemContract } from '@cpn-console/shared' +import { serverInstance } from '@/app.js' +import { appVersion } from '@/utils/env.js' + +export function systemRouter() { + return serverInstance.router(systemContract, { + getVersion: async () => ({ + status: 200, + body: { + version: appVersion, + }, + }), + + getHealth: async () => ({ + status: 200, + body: { + status: 'OK', + }, + }), + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts new file mode 100644 index 000000000..5e562353b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts @@ -0,0 +1,9 @@ +import type { UpsertSystemSettingBody } from '@cpn-console/shared' +import { + getSystemSettings as getSystemSettingsQuery, + upsertSystemSetting as upsertSystemSettingQuery, +} from './queries.js' + +export const getSystemSettings = (key?: string) => getSystemSettingsQuery({ key }) + +export const upsertSystemSetting = (newSystemSetting: UpsertSystemSettingBody) => upsertSystemSettingQuery(newSystemSetting) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts new file mode 100644 index 000000000..c64cb3b74 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts @@ -0,0 +1,18 @@ +import type { Prisma, SystemSetting } from '@prisma/client' +import prisma from '@/prisma.js' + +export function upsertSystemSetting(newSystemSetting: SystemSetting) { + return prisma.systemSetting.upsert({ + create: { + ...newSystemSetting, + }, + update: { + value: newSystemSetting.value, + }, + where: { + key: newSystemSetting.key, + }, + }) +} + +export const getSystemSettings = (where?: Prisma.SystemSettingWhereInput) => prisma.systemSetting.findMany({ where }) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts new file mode 100644 index 000000000..c4c6b56b6 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { systemSettingsContract } from '@cpn-console/shared' +import app from '../../../app.js' +import * as utilsController from '../../../utils/controller.js' +import { getUserMockInfos } from '../../../utils/mocks.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetSystemSettingsMock = vi.spyOn(business, 'getSystemSettings') +const businessUpsertSystemSettingMock = vi.spyOn(business, 'upsertSystemSetting') + +describe('test systemSettingsContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listSystemSettings', () => { + it('should return plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true) + const systemSettings = [] + + authUserMock.mockResolvedValueOnce(user) + businessGetSystemSettingsMock.mockResolvedValueOnce(systemSettings) + + const response = await app.inject() + .get(systemSettingsContract.listSystemSettings.path) + .end() + + expect(businessGetSystemSettingsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(systemSettings) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('upsertSystemSetting', () => { + const newConfig = { key: 'key1', value: 'value1' } + it('should update system setting, authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessUpsertSystemSettingMock.mockResolvedValueOnce(newConfig) + + const response = await app.inject() + .post(systemSettingsContract.upsertSystemSetting.path) + .body(newConfig) + .end() + + expect(businessUpsertSystemSettingMock).toHaveBeenCalledWith(newConfig) + expect(response.statusCode).toEqual(201) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(systemSettingsContract.upsertSystemSetting.path) + .body(newConfig) + .end() + + expect(businessUpsertSystemSettingMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts new file mode 100644 index 000000000..baa0765b6 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts @@ -0,0 +1,30 @@ +import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared' +import { getSystemSettings, upsertSystemSetting } from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { Forbidden403 } from '@/utils/errors.js' + +export function systemSettingsRouter() { + return serverInstance.router(systemSettingsContract, { + listSystemSettings: async ({ query }) => { + const systemSettings = await getSystemSettings(query.key) + + return { + status: 200, + body: systemSettings, + } + }, + + upsertSystemSetting: async ({ request: req, body: data }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const systemSetting = await upsertSystemSetting(data) + + return { + status: 201, + body: systemSetting, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts new file mode 100644 index 000000000..50e1dd20c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts @@ -0,0 +1,222 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import prisma from '../../__mocks__/prisma.js' +import type { UserDetails } from '../../types/index.ts' +import { TokenInvalidReason, getMatchingUsers, getUsers, logViaSession, logViaToken, patchUsers } from './business.ts' +import * as queries from './queries.js' + +const getUsersQueryMock = vi.spyOn(queries, 'getUsers') +const getMatchingUsersQueryMock = vi.spyOn(queries, 'getMatchingUsers') + +describe('test users business', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + const user = { + adminRoleIds: [], + createdAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + id: faker.string.uuid(), + lastName: faker.person.lastName(), + updatedAt: new Date(), + } + const projectId = faker.string.uuid() + const adminRoleId = faker.string.uuid() + describe('patchUsers', () => { + it('should do nothing', async () => { + prisma.user.update.mockResolvedValue(null) + + await patchUsers([]) + + expect(prisma.user.update).toHaveBeenCalledTimes(0) + }) + + it('should update a user adminRoleIds', async () => { + const userUpdated = { id: user.id, adminRoleIds: user.adminRoleIds } + + prisma.user.update.mockResolvedValue(user) + + prisma.user.findMany.mockResolvedValue([]) + + await patchUsers([userUpdated]) + expect(prisma.user.update).toHaveBeenCalledTimes(1) + expect(prisma.user.findMany).toHaveBeenCalledTimes(1) + + await patchUsers([userUpdated, userUpdated]) + expect(prisma.user.update).toHaveBeenCalledTimes(3) + }) + }) + describe('getUsers', () => { + it('should query without where', async () => { + prisma.user.update.mockResolvedValue(null) + + await getUsers({}) + + expect(getUsersQueryMock).toHaveBeenCalledTimes(1) + expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [] }) + }) + it('should query with filter adminRoleIds', async () => { + prisma.user.update.mockResolvedValue(null) + + await getUsers({ adminRoleIds: [adminRoleId] }) + + expect(getUsersQueryMock).toHaveBeenCalledTimes(1) + expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [{ adminRoleIds: { hasEvery: [adminRoleId] } }] }) + }) + }) + + describe('getMatchingUsers', () => { + const AND = [ + { + OR: [ + { + email: { + contains: 'abc', + mode: 'insensitive', + }, + }, + { + firstName: { + contains: 'abc', + mode: 'insensitive', + }, + }, + { + lastName: { + contains: 'abc', + mode: 'insensitive', + }, + }, + ], + }, + { + type: 'human', + }, + ] + it('should query only with letters ', async () => { + prisma.user.update.mockResolvedValue(null) + + await getMatchingUsers({ letters: 'abc' }) + + expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1) + expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND }) + }) + it('should query with letters and projectId', async () => { + prisma.user.update.mockResolvedValue(null) + + await getMatchingUsers({ letters: 'abc', notInProjectId: projectId }) + + expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1) + expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND: [{ + projectMembers: { + none: { + projectId, + }, + }, + }, { + projectsOwned: { + none: { + id: projectId, + }, + }, + }].concat(AND) }) + }) + }) + describe('logViaSession', () => { + // ça ne teste pas tout mais c'est déjà bien hein + const adminRoles = [{ + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: 0n, + position: 0, + }, { + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '/admin', + permissions: 0n, + position: 0, + }] + const userToLog: UserDetails = { + id: faker.string.uuid(), + email: user.email, + firstName: user.firstName, + groups: [], + lastName: user.lastName, + } + it('should create user and return adminPerms', async () => { + prisma.adminRole.findMany.mockResolvedValue(adminRoles) + prisma.user.findUnique.mockResolvedValue(undefined) + prisma.user.create.mockResolvedValue(user) + prisma.user.update.mockResolvedValue(user) + const response = await logViaSession(userToLog) + expect(response.adminPerms).toBe(0n) + expect(prisma.user.create).toHaveBeenCalledTimes(1) + }) + it('should update user and return adminPerms', async () => { + prisma.adminRole.findMany.mockResolvedValue(adminRoles) + prisma.user.findUnique.mockResolvedValue(user) + prisma.user.update.mockResolvedValue(user) + const response = await logViaSession(userToLog) + expect(response.adminPerms).toEqual(0n) + expect(prisma.user.create).toHaveBeenCalledTimes(0) + }) + }) +}) + +describe('logViaToken', () => { + const nextYear = new Date() + const lastYear = new Date() + nextYear.setFullYear((new Date()).getFullYear() + 1) + lastYear.setFullYear((new Date()).getFullYear() - 1) + const baseToken = { + createdAt: new Date(), + hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + id: faker.string.uuid(), + lastUse: null, + permissions: 2n, + userId: null, + status: 'active', + } as const + + it('should return identity', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken }) + const identity = await logViaToken('test') + expect(identity.adminPerms).toBe(2n) + }) + + it('should return identity based on pat', async () => { + const pat = structuredClone(baseToken) + delete pat.permissions + pat.owner = { adminRoleIds: null } + prisma.personalAccessToken.findFirst.mockResolvedValueOnce(pat) + const identity = await logViaToken('test') + expect(identity.adminPerms).toBe(0n) + }) + + it('should return identity, with expirationDate', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, expirationDate: nextYear }) + const identity = await logViaToken('test') + expect(identity.adminPerms).toBe(2n) + }) + + it('should return cause revoked', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, status: 'revoked' }) + const identity = await logViaToken('test') + expect(identity).toBe(TokenInvalidReason.INACTIVE) + }) + + it('should return cause expired', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, expirationDate: lastYear }) + const identity = await logViaToken('test') + expect(identity).toBe(TokenInvalidReason.EXPIRED) + }) + + it('should return cause not found', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce(undefined) + const identity = await logViaToken('test') + expect(identity).toBe(TokenInvalidReason.NOT_FOUND) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts new file mode 100644 index 000000000..ddaef0a01 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts @@ -0,0 +1,201 @@ +import { createHash } from 'node:crypto' +import type { AdminRole, AdminToken, PersonalAccessToken, Prisma, User } from '@prisma/client' +import type { XOR, userContract } from '@cpn-console/shared' +import { getMatchingUsers as getMatchingUsersQuery, getUsers as getUsersQuery } from '@/resources/queries-index.js' +import prisma from '@/prisma.js' +import type { UserDetails } from '@/types/index.js' +import { BadRequest400 } from '@/utils/errors.js' + +export async function getUsers(query: typeof userContract.getAllUsers.query._type, relationType: 'OR' | 'AND' = 'AND') { + const whereInputs: Prisma.UserWhereInput[] = [] + if (query.adminRoleIds?.length) { + whereInputs.push({ adminRoleIds: { hasEvery: query.adminRoleIds } }) + } + if (query.adminRoles?.length) { + const roles = query.adminRoles + ? await prisma.adminRole.findMany({ where: { name: { in: query.adminRoles } } }) + : [] + + const adminRoleNameNotFound = query.adminRoles?.find(nameQueried => !roles.find(({ name }) => name === nameQueried)) + if (adminRoleNameNotFound) { + return new BadRequest400(`Unable to find adminRole ${adminRoleNameNotFound}`) + } + whereInputs.push({ adminRoleIds: { hasEvery: roles.map(({ id }) => id) } }) + } + if (query.memberOfIds) { + whereInputs.push({ + AND: query.memberOfIds.map(id => ({ + OR: [ + { projectsOwned: { some: { id } } }, + { ProjectMembers: { some: { project: { id } } } }, + ], + })), + }) + } + + return getUsersQuery({ [relationType]: whereInputs }) +} + +export async function getMatchingUsers(query: typeof userContract.getMatchingUsers.query._type) { + const AND: Prisma.UserWhereInput[] = [] + if (query.notInProjectId) { + AND.push({ projectMembers: { none: { projectId: query.notInProjectId } } }) + AND.push({ projectsOwned: { none: { id: query.notInProjectId } } }) + } + const filter = { contains: query.letters, mode: 'insensitive' } as const // Default value: default + if (query.letters) { + AND.push({ + OR: [{ + email: filter, + }, { + firstName: filter, + }, { + lastName: filter, + }], + }) + AND.push({ type: 'human' }) + } + + return getMatchingUsersQuery({ + AND, + }) +} + +export async function patchUsers(users: typeof userContract.patchUsers.body._type) { + for (const user of users) { + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + adminRoleIds: user.adminRoleIds, + }, + }) + } + + return prisma.user.findMany({ + where: { + id: { in: users.map(({ id }) => id) }, + }, + }) +} + +export enum TokenInvalidReason { + INACTIVE = 'Not active', + EXPIRED = 'Expired', + NOT_FOUND = 'Not authenticated', +} + +type UserTrial = Omit +export async function logViaSession({ id, email, groups, ...user }: UserTrial): Promise<{ user: User, adminPerms: bigint }> { + let userDb = await prisma.user.findUnique({ + where: { id }, + }) + + if (!userDb) { + userDb = await prisma.user.create({ data: { email, id, ...user, adminRoleIds: [], type: 'human' } }) + } + + const matchingAdminRoles = await prisma.adminRole.findMany({ + where: { OR: [{ oidcGroup: { in: groups } }, { id: { in: userDb.adminRoleIds } }] }, + }) + + const oidcRoleIds = matchingAdminRoles + .filter(({ oidcGroup }) => oidcGroup && groups.includes(oidcGroup)) + .map(({ id }) => id) + + const nonOidcRoleIds = matchingAdminRoles + .filter(({ oidcGroup, id }) => !oidcGroup && userDb.adminRoleIds.includes(id)) + .map(({ id }) => id) + + // On enregistre en bdd uniquement les roles de l'utilisateur + // qui ne viennent pas de keycloak + const updatedUser = await prisma.user.update({ where: { id }, data: { ...user, adminRoleIds: nonOidcRoleIds, lastLogin: (new Date()).toISOString() } }) + .then(user => ({ ...user, adminRoleIds: [...user.adminRoleIds, ...oidcRoleIds] })) + return { + user: updatedUser, + adminPerms: sumAdminPerms(matchingAdminRoles), + } +} + +type UserWithTokenId = Omit & { tokenId: string } +export async function logViaToken(pass: string): Promise<({ user: UserWithTokenId, adminPerms: bigint }) | TokenInvalidReason> { + const passHash = createHash('sha256').update(pass).digest('hex') + + let token: (XOR & { owner: User }) | TokenInvalidReason | undefined + const tokenLoginMethods = [findPersonalAccessToken, findAdminToken] + for (const tokenLoginMethod of tokenLoginMethods) { + token = await tokenLoginMethod(passHash) + if (token) { + break + } + } + + if (typeof token === 'string') { + return token + } + if (!token) { + return TokenInvalidReason.NOT_FOUND + } + + return { + user: { + ...token.owner, + tokenId: token.id, + }, + adminPerms: token?.permissions ?? await getAdminRolesAndSum(token.owner.adminRoleIds), + } +} + +function isTokenInvalid(token: AdminToken | PersonalAccessToken): TokenInvalidReason | undefined { + if (token.status !== 'active') { + return TokenInvalidReason.INACTIVE + } + const currentDate = new Date() + if (token.expirationDate && currentDate.getTime() > token.expirationDate?.getTime()) { + return TokenInvalidReason.EXPIRED + } +} + +function sumAdminPerms(roles: AdminRole[]): bigint { + if (!roles.length) { + return 0n + } + return roles.reduce((acc, curr) => acc | curr.permissions, 0n) +} + +async function getAdminRolesAndSum(roles: AdminRole['id'][] | null): Promise { + if (!roles?.length) { + return 0n + } + return sumAdminPerms(await prisma.adminRole.findMany({ + where: { id: { in: roles } }, + })) +} + +// List all token tpe authentication +async function findPersonalAccessToken(digest: string): Promise<(PersonalAccessToken & { owner: User }) | undefined | TokenInvalidReason> { + const token = await prisma.personalAccessToken.findFirst({ where: { hash: digest }, include: { owner: true } }) + if (!token) + return undefined + const invalidReason = isTokenInvalid(token) + if (invalidReason) { + return invalidReason + } + await prisma.personalAccessToken.update({ where: { id: token.id }, data: { lastUse: (new Date()).toISOString() } }) + await prisma.user.update({ where: { id: token.owner.id }, data: { lastLogin: (new Date()).toISOString() } }) + return token +} + +async function findAdminToken(digest: string): Promise<(AdminToken & { owner: User }) | undefined | TokenInvalidReason> { + const token = await prisma.adminToken.findFirst({ where: { hash: digest }, include: { owner: true } }) + if (!token) + return undefined + const invalidReason = isTokenInvalid(token) + if (invalidReason) { + return invalidReason + } + await prisma.adminToken.update({ where: { id: token.id }, data: { lastUse: (new Date()).toISOString() } }) + await prisma.user.update({ where: { id: token.userId }, data: { lastLogin: (new Date()).toISOString() } }) + return token +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts new file mode 100644 index 000000000..1e33a8128 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts @@ -0,0 +1,60 @@ +import type { Prisma, User } from '@prisma/client' +import prisma from '@/prisma.js' + +type UserCreate = Omit + +// SELECT +export const getUsers = (where?: Prisma.UserWhereInput) => prisma.user.findMany({ where }) + +export async function getUserInfos(id: User['id']) { + return prisma.user.findMany({ + where: { id }, + include: { + logs: true, + }, + }) +} + +export function getMatchingUsers(where: Prisma.UserWhereInput) { + return prisma.user.findMany({ + where, + take: 5, + }) +} + +export function getUserById(id: User['id']) { + return prisma.user.findUnique({ where: { id } }) +} + +export function getUserOrThrow(id: User['id']) { + return prisma.user.findUniqueOrThrow({ + where: { id }, + }) +} + +export function getUserByEmail(email: User['email']) { + return prisma.user.findUnique({ where: { email } }) +} + +// CREATE +export async function createUser({ id, email, firstName, lastName, type }: UserCreate) { + const user = await getUserByEmail(email) + if (user) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') + return prisma.user.create({ data: { id, email, firstName, lastName, type } }) +} + +// UPDATE +export async function updateUserById({ id, email, firstName, lastName }: UserCreate) { + const user = await getUserById(id) + const isEmailAlreadyTaken = await getUserByEmail(email) + if (!user) throw new Error('L\'utilisateur demandé n\'existe pas') + if (isEmailAlreadyTaken) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') + if (user && !isEmailAlreadyTaken) { + return prisma.user.update({ where: { id }, data: { email, firstName, lastName } }) + } +} + +// TECH +export function _createUser(data: Prisma.UserCreateInput) { + return prisma.user.upsert({ where: { id: data.id }, create: data, update: data }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts new file mode 100644 index 000000000..ffa817afe --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { userContract } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getUserMockInfos, setRequestor } from '../../utils/mocks.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetMatchingMock = vi.spyOn(business, 'getMatchingUsers') +const businessLogViaSessionMock = vi.spyOn(business, 'logViaSession') +const businessGetUsersMock = vi.spyOn(business, 'getUsers') +const businessPatchMock = vi.spyOn(business, 'patchUsers') + +describe('test userContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('getMatchingUsers', () => { + it('should return matching users', async () => { + const usersMatching = [] + businessGetMatchingMock.mockResolvedValueOnce(usersMatching) + + const response = await app.inject() + .get(userContract.getMatchingUsers.path) + .query({ letters: faker.person.fullName() }) + .end() + + expect(businessGetMatchingMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(usersMatching) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('auth', () => { + it('should return logged user', async () => { + const user = { + id: faker.string.uuid(), + adminRoleIds: [], + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + type: 'human', + lastName: faker.person.lastName(), + } + setRequestor(user) + businessLogViaSessionMock.mockResolvedValueOnce({ user, adminPerms: 0n }) + + const response = await app.inject() + .get(userContract.auth.path) + .end() + + expect(businessLogViaSessionMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(user) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('getAllUsers', () => { + it('should return all users for admin', async () => { + const user = getUserMockInfos(true) + const users = [] + authUserMock.mockResolvedValueOnce(user) + businessGetUsersMock.mockResolvedValueOnce(users) + + const response = await app.inject() + .get(userContract.getAllUsers.path) + .query({ role: 'admin' }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetUsersMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(users) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(userContract.getAllUsers.path) + .query({ role: 'admin' }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetUsersMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('patchUsers', () => { + const usersPatchData = [{ + id: faker.string.uuid(), + adminRoleIds: [], + }] + const usersReturn = [{ + id: faker.string.uuid(), + adminRoleIds: [], + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + type: 'human', + }] + + it('should patch and return users for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessPatchMock.mockResolvedValueOnce(usersReturn) + const response = await app.inject() + .patch(userContract.patchUsers.path) + .body(usersPatchData) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessPatchMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(usersReturn) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(userContract.patchUsers.path) + .body(usersPatchData) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessPatchMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts new file mode 100644 index 000000000..c95d0620c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts @@ -0,0 +1,63 @@ +import { AdminAuthorized, userContract } from '@cpn-console/shared' +import { + getMatchingUsers, + getUsers, + logViaSession, + patchUsers, +} from './business.js' +import '@/types/index.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@/utils/errors.js' + +export function userRouter() { + return serverInstance.router(userContract, { + getMatchingUsers: async ({ query }) => { + const usersMatching = await getMatchingUsers(query) + + return { + status: 200, + body: usersMatching, + } + }, + + auth: async ({ request: req }) => { + const user = req.session.user + + if (!user) return new Unauthorized401() + + const { user: body } = await logViaSession(user) + + return { + status: 200, + body, + } + }, + + getAllUsers: async ({ request: req, query: { relationType, ...query } }) => { + const perms = await authUser(req) + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const body = await getUsers(query, relationType) + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + patchUsers: async ({ request: req, body }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const users = await patchUsers(body) + + return { + status: 200, + body: users, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts new file mode 100644 index 000000000..4e6da3e6e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts @@ -0,0 +1,51 @@ +import { createHash } from 'node:crypto' +import type { personalAccessTokenContract } from '@cpn-console/shared' +import { generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' +import type { AdminToken, User } from '@prisma/client' +import prisma from '../../../prisma.js' +import { BadRequest400 } from '@/utils/errors.js' + +export async function listTokens(userId: User['id']) { + return prisma.personalAccessToken.findMany({ + omit: { hash: true }, + include: { owner: true }, + orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], + where: { userId }, + }) +} + +export async function createToken(data: typeof personalAccessTokenContract.createPersonalAccessToken.body._type, userId: User['id']) { + if (data.expirationDate && !isAtLeastTomorrow(new Date(data.expirationDate))) { + return new BadRequest400('Date d\'expiration trop courte') + } + const password = generateRandomPassword(48, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') + const hash = createHash('sha256').update(password).digest('hex') + const token = await prisma.personalAccessToken.create({ + data: { + ...data, + hash, + expirationDate: new Date(data.expirationDate), + userId, + }, + omit: { hash: true }, + include: { owner: true }, + }) + return { + ...token, + password, + } +} + +export async function deleteToken(id: AdminToken['id'], userId: User['id']) { + const token = await prisma.personalAccessToken.findUnique({ + where: { + id, + userId, + }, + }) + if (token) { + return prisma.personalAccessToken.delete({ + where: { id }, + }) + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts new file mode 100644 index 000000000..4dfdc134d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts @@ -0,0 +1,48 @@ +import { personalAccessTokenContract } from '@cpn-console/shared' + +import '@/types/index.js' +import { createToken, deleteToken, listTokens } from './business.js' +import { serverInstance } from '@/app.js' +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@/utils/errors.js' + +export function personalAccessTokenRouter() { + return serverInstance.router(personalAccessTokenContract, { + listPersonalAccessTokens: async ({ request: req }) => { + const perms = await authUser(req) + + if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + const body = await listTokens(perms.user.id) + + return { + status: 200, + body, + } + }, + + createPersonalAccessToken: async ({ request: req, body: data }) => { + const perms = await authUser(req) + + if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + const body = await createToken(data, perms.user.id) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + deletePersonalAccessToken: async ({ request: req, params }) => { + const perms = await authUser(req) + + if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + await deleteToken(params.tokenId, perms.user.id) + + return { + status: 204, + body: null, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts new file mode 100644 index 000000000..b126fce18 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts @@ -0,0 +1,133 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Cluster, Zone } from '@prisma/client' +import prisma from '../../__mocks__/prisma.js' +import { BadRequest400 } from '../../utils/errors.ts' +import { hook } from '../../__mocks__/utils/hook-wrapper.ts' +import { createZone, deleteZone, listZones, updateZone } from './business.ts' +import * as queries from './queries.js' + +const userId = faker.string.uuid() +const reqId = faker.string.uuid() +const linkZoneToClustersMock = vi.spyOn(queries, 'linkZoneToClusters') +vi.mock('../../utils/hook-wrapper.ts', async () => ({ + hook, +})) + +describe('test zone business', () => { + const zones: Zone[] = [{ + id: faker.string.uuid(), + label: faker.company.name(), + argocdUrl: faker.internet.url(), + createdAt: new Date(), + updatedAt: new Date(), + description: faker.lorem.lines(1), + slug: faker.string.alphanumeric(5), + }, { + id: faker.string.uuid(), + label: faker.company.name(), + argocdUrl: faker.internet.url(), + createdAt: new Date(), + updatedAt: new Date(), + description: faker.lorem.lines(1), + slug: faker.string.alphanumeric(6), + }] + + const clusters: Pick[] = [ + { id: faker.string.uuid() }, + { id: faker.string.uuid() }, + ] + + beforeEach(() => { + vi.resetAllMocks() + }) + describe('listZones', () => { + it('should return zones', async () => { + prisma.zone.findMany.mockResolvedValueOnce(zones) + + const response = await listZones() + expect(response).toEqual(zones) + }) + }) + describe('createZone', () => { + it('should create zone without description and clusterIds', async () => { + const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl } + + hook.zone.upsert.mockResolvedValue({}) + prisma.zone.create.mockResolvedValueOnce(zones[0]) + const response = await createZone(newZone, userId, reqId) + + expect(response).toEqual(zones[0]) + expect(prisma.zone.create).toHaveBeenCalledWith({ + data: { + slug: newZone.slug, + label: newZone.label, + argocdUrl: newZone.argocdUrl, + description: undefined, + }, + }) + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0) + }) + it('should create zone with description and clusterIds', async () => { + const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl, clusterIds: clusters.map(({ id }) => id), description: faker.lorem.lines(2) } + + hook.zone.upsert.mockResolvedValue({}) + prisma.zone.create.mockResolvedValueOnce(zones[0]) + const response = await createZone(newZone, userId, reqId) + + expect(response).toEqual(zones[0]) + expect(prisma.zone.create).toHaveBeenCalledWith({ + data: { + description: newZone.description, + label: newZone.label, + argocdUrl: newZone.argocdUrl, + slug: newZone.slug, + }, + }) + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(1) + }) + it('should not create zone, conflict label', async () => { + const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl } + + prisma.zone.findUnique.mockResolvedValueOnce(zones[0]) + prisma.zone.create.mockResolvedValueOnce(zones[0]) + const response = await createZone(newZone, userId, reqId) + + expect(response).instanceOf(BadRequest400) + expect(prisma.zone.create).toHaveBeenCalledTimes(0) + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0) + }) + }) + describe('updateZone', () => { + it('should filter keys and update zone', async () => { + prisma.zone.update.mockResolvedValueOnce(zones[0]) + hook.zone.upsert.mockResolvedValue({}) + await updateZone(zones[0].id, { + description: '', + label: zones[0].label, + argocdUrl: zones[0].argocdUrl, + extraKey: 1, + }, userId, reqId) + expect(prisma.zone.update).toHaveBeenCalledWith({ where: { id: zones[0].id }, data: { + description: '', + label: zones[0].label, + argocdUrl: zones[0].argocdUrl, + } }) + }) + }) + describe('deleteZone', () => { + it('should not delete zone, cluster attached', async () => { + prisma.cluster.findFirst.mockResolvedValueOnce(clusters[0]) + const response = await deleteZone(zones[0].id, userId, reqId) + expect(response).instanceOf(BadRequest400) + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) + }) + it('should delete zone', async () => { + prisma.cluster.findFirst.mockResolvedValueOnce(undefined) + hook.zone.delete.mockResolvedValue({}) + const response = await deleteZone(zones[0].id, userId, reqId) + expect(response).toEqual(null) + expect(prisma.zone.delete).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts new file mode 100644 index 000000000..c0cd25979 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts @@ -0,0 +1,78 @@ +import type { User, Zone } from '@cpn-console/shared' +import { addLogs } from '../queries-index.js' +import { linkZoneToClusters } from './queries.js' +import { BadRequest400, Unprocessable422 } from '@/utils/errors.js' +import prisma from '@/prisma.js' +import { hook } from '@/utils/hook-wrapper.js' + +export const listZones = prisma.zone.findMany + +export async function createZone( + data: { slug: string, label: string, argocdUrl: string, description?: string | null, clusterIds?: string[] }, + userId: User['id'], + requestId: string, +) { + const { slug, label, argocdUrl, description, clusterIds } = data + + const existingZone = await prisma.zone.findUnique({ + where: { slug }, + }) + + if (existingZone) return new BadRequest400(`Une zone portant le nom ${slug} existe déjà.`) + const zone = await prisma.zone.create({ + data: { + slug, + label, + argocdUrl, + description, + }, + }) + if (clusterIds) { + await linkZoneToClusters(zone.id, clusterIds) + } + const hookReply = await hook.zone.upsert(zone.id) + await addLogs({ action: 'Create zone', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services lors de la création de la zone') + } + return zone +} + +export async function updateZone( + zoneId: Zone['id'], + data: Pick, + userId: User['id'], + requestId: string, +) { + const { label, argocdUrl, description } = data + + const updatedZone = await prisma.zone.update({ + where: { + id: zoneId, + }, + data: { + label, + argocdUrl, + description, + }, + }) + const hookReply = await hook.zone.upsert(updatedZone.id) + await addLogs({ action: 'Update zone', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services lors de la mise à jour de la zone') + } + return updatedZone +} + +export async function deleteZone(zoneId: Zone['id'], userId: User['id'], requestId: string) { + const attachedCluster = await prisma.cluster.findFirst({ where: { zoneId }, select: { id: true } }) + if (attachedCluster) return new BadRequest400('Vous ne pouvez supprimer cette zone, car des clusters y sont associés.') + + const hookReply = await hook.zone.delete(zoneId) + await addLogs({ action: 'Delete zone', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services lors de la suppression de la zone') + } + await prisma.zone.delete({ where: { id: zoneId } }) + return null +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts new file mode 100644 index 000000000..1390bb153 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts @@ -0,0 +1,21 @@ +import type { Cluster, Zone } from '@prisma/client' +import prisma from '@/prisma.js' + +export function getZoneByIdOrThrow(id: Zone['id']) { + return prisma.zone.findUniqueOrThrow({ + where: { id }, + }) +} + +export function linkZoneToClusters(zoneId: Zone['id'], clusterIds: Cluster['id'][]) { + return prisma.zone.update({ + where: { + id: zoneId, + }, + data: { + clusters: { + connect: clusterIds.map(clusterId => ({ id: clusterId })), + }, + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts new file mode 100644 index 000000000..7cec26cd3 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts @@ -0,0 +1,162 @@ +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Zone } from '@cpn-console/shared' +import { zoneContract } from '@cpn-console/shared' +import app from '../../app.js' +import * as utilsController from '../../utils/controller.js' +import { getUserMockInfos } from '../../utils/mocks.js' +import { BadRequest400 } from '../../utils/errors.js' +import * as business from './business.js' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMock = vi.spyOn(business, 'listZones') +const businessCreateMock = vi.spyOn(business, 'createZone') +const businessUpdateMock = vi.spyOn(business, 'updateZone') +const businessDeleteMock = vi.spyOn(business, 'deleteZone') + +describe('test zoneContract', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listZones', () => { + it('should return list of zones', async () => { + const zones = [] + businessListMock.mockResolvedValueOnce(zones) + + const response = await app.inject() + .get(zoneContract.listZones.path) + .end() + + expect(businessListMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(zones) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('createZone', () => { + const zone = { id: faker.string.uuid(), label: faker.string.alpha({ length: 5 }), argocdUrl: faker.internet.url(), slug: faker.string.alpha({ length: 5, casing: 'lower' }), description: '' } + + it('should create and return zone for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(zone) + const response = await app.inject() + .post(zoneContract.createZone.path) + .body(zone) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(zone) + expect(response.statusCode).toEqual(201) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(zoneContract.createZone.path) + .body(zone) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(zoneContract.createZone.path) + .body(zone) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('updateZone', () => { + const zoneId = faker.string.uuid() + const zone: Omit = { label: faker.string.alpha({ length: 5 }), slug: faker.string.alpha({ length: 5, casing: 'lower' }), argocdUrl: faker.internet.url(), description: '' } + + it('should update and return zone for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: zoneId, ...zone }) + const response = await app.inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ id: zoneId, ...zone }) + expect(response.statusCode).toEqual(200) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('deleteZone', () => { + it('should delete zone for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(1) + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts new file mode 100644 index 000000000..da9931329 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts @@ -0,0 +1,64 @@ +import { AdminAuthorized, zoneContract } from '@cpn-console/shared' +import { createZone, deleteZone, listZones, updateZone } from './business.js' +import { serverInstance } from '@/app.js' + +import { authUser } from '@/utils/controller.js' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@/utils/errors.js' + +export function zoneRouter() { + return serverInstance.router(zoneContract, { + listZones: async () => { + const zones = await listZones() + + return { + status: 200, + body: zones, + } + }, + + createZone: async ({ request: req, body: data }) => { + const { user, adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user) return new Unauthorized401('Require to be requested from user not api key') + + const body = await createZone(data, user.id, req.id) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + updateZone: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user) return new Unauthorized401('Require to be requested from user not api key') + + const zoneId = params.zoneId + + const body = await updateZone(zoneId, data, user.id, req.id) + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + deleteZone: async ({ request: req, params }) => { + const { user, adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user) return new Unauthorized401('Require to be requested from user not api key') + const zoneId = params.zoneId + + const body = await deleteZone(zoneId, user.id, req.id) + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts new file mode 100644 index 000000000..8620cc2fa --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts @@ -0,0 +1,57 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { exitGracefully, handleExit } from './server.js' +import { closeConnections } from './connect.js' +import { logger } from './app.js' + +vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) +vi.mock('./init/db/index.js', () => ({ initDb: vi.fn() })) +vi.mock('./connect.js') + +process.exit = vi.fn() + +vi.mock('./prepare-app.js', () => { + const app = { + listen: vi.fn(), + close: vi.fn(async () => {}), + } + return { + getPreparedApp: () => Promise.resolve(app), + } +}) +vi.spyOn(logger, 'info') +vi.spyOn(logger, 'warn') +vi.spyOn(logger, 'error') +vi.spyOn(logger, 'fatal') +vi.spyOn(logger, 'debug') + +describe('server', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should call closeConnections without parameter', async () => { + await exitGracefully() + + expect(closeConnections).toHaveBeenCalledTimes(1) + expect(closeConnections.mock.calls[0]).toHaveLength(0) + expect(logger.error).toHaveBeenCalledTimes(0) + }) + + it('should log an error', async () => { + await exitGracefully(new Error('error')) + + expect(closeConnections).toHaveBeenCalledTimes(1) + expect(closeConnections.mock.calls[0]).toHaveLength(0) + expect(logger.fatal).toHaveBeenCalledTimes(1) + expect(logger.fatal.mock.calls[0][0]).toBeInstanceOf(Error) + expect(logger.info).toHaveBeenCalledTimes(2) + }) + + it('should call process.on 4 times', () => { + const processOn = vi.spyOn(process, 'on') + + handleExit() + + expect(processOn).toHaveBeenCalledTimes(5) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts new file mode 100644 index 000000000..7d1497a8e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts @@ -0,0 +1,44 @@ +import { getPreparedApp } from './prepare-app.js' +import { closeConnections } from './connect.js' +import { isCI, isDev, isDevSetup, isProd, isTest, port } from './utils/env.js' +import { logger } from './app.js' + +const app = await getPreparedApp() + +try { + await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }) +} catch (error) { + logger.error(error) + process.exit(1) +} + +logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) + +export async function exitGracefully(error?: Error) { + if (error instanceof Error) { + logger.fatal(error) + } + await app.close() + logger.info('Closing connections...') + await closeConnections() + logger.info('Exiting...') + process.exit(error instanceof Error ? 1 : 0) +} + +function logExitCode(code: number) { + logger.warn(`received signal: ${code}`) +} + +function logUnhandledRejection(reason: unknown, promise: Promise) { + logger.error({ message: 'Unhandled Rejection', promise, reason }) +} + +export function handleExit() { + process.on('exit', logExitCode) + process.on('SIGINT', exitGracefully) + process.on('SIGTERM', exitGracefully) + process.on('uncaughtException', exitGracefully) + process.on('unhandledRejection', logUnhandledRejection) +} + +handleExit() diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts new file mode 100644 index 000000000..282b1c43d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts @@ -0,0 +1,41 @@ +import { type SharedSafeParseReturnType, parseZodError } from '@cpn-console/shared' +import { BadRequest400 } from './errors.js' + +export type Success = Result +export type Failure = Result +export class Result { + protected constructor( + readonly success: boolean, + readonly value: T | string, + ) {} + + static succeed(value: T): Success { + return new Result(true, value) as Success + } + + static fail(message: string): Failure { + return new Result(false, message) as Failure + } + + get isSuccess(): boolean { + return this.success + } + + get isError(): boolean { + return !this.success + } + + get data(): T { + if (this.success) return this.value as T + throw new Error('Cannot get data from a Failure') + } + + get error(): string { + if (!this.success) return this.value as string + throw new Error('Cannot get error from a Success') + } +} + +export function validateSchema(schemaValidation: SharedSafeParseReturnType) { + if (!schemaValidation.success) return new BadRequest400(parseZodError(schemaValidation.error)) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts new file mode 100644 index 000000000..c5b1c72f0 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts @@ -0,0 +1,169 @@ +import type { Cluster, Prisma, Project, ProjectMembers, ProjectRole } from '@prisma/client' +import type { XOR } from '@cpn-console/shared' +import { PROJECT_PERMS as PP, PROJECT_PERMS, projectIsLockedInfo, tokenHeaderName } from '@cpn-console/shared' +import type { FastifyRequest } from 'fastify' +import { Unauthorized401 } from './errors.js' +import { uuid } from './queries-tools.js' +import type { UserDetails } from '@/types/index.js' +import prisma from '@/prisma.js' +import { logViaSession, logViaToken } from '@/resources/user/business.js' + +export type RequireOnlyOne = + Pick> + & { + [K in Keys]-?: + Required> + & Partial, undefined>> + }[Keys] + +type ErrorMessagePredicate = () => string | undefined +export function getErrorMessage(...fns: ErrorMessagePredicate[]) { + for (const f of fns) { + const error = f() + if (error) { + return error + } + } +} + +/** + * Renvoie une erreur si le projet est verrouillé + */ +export function checkProjectLocked(project: { locked: boolean }): string { + return project.locked + ? projectIsLockedInfo + : '' +} + +export function checkLocked(project: { locked: Project['locked'] }): string { + return checkProjectLocked(project) +} + +export function checkClusterUnavailable(clusterId: Cluster['id'], authorizedClusterIds: Cluster['id'][]): string { + return authorizedClusterIds.includes(clusterId) + ? '' + : 'Ce cluster n\'est pas disponible pour cette combinaison projet et stage' +} + +export const splitStringsFilterArray = >(toMatch: T, inputs: string): T => inputs.split(',').filter(i => toMatch.includes(i)) as unknown as T + +type StringArray = string[] +interface WhereBuilderParams { + enumValues: T + eqValue: T[number] | undefined + inValues: string | undefined + notInValues: string | undefined +} + +export function whereBuilder({ enumValues, eqValue, inValues, notInValues }: WhereBuilderParams) { + if (eqValue) { + return eqValue + } else if (inValues) { + return { in: splitStringsFilterArray(enumValues, inValues) } + } else if (notInValues) { + return { notIn: splitStringsFilterArray(enumValues, notInValues) } + } +} + +type ProjectMinimalPerms = Pick & { roles: ProjectRole[], members: ProjectMembers[] } +export interface UserProfile { user?: UserDetails, adminPermissions: bigint, tokenId?: string } +export interface ProjectPermState { projectPermissions?: bigint, projectId: Project['id'], projectLocked: boolean, projectStatus: Project['status'], projectOwnerId: Project['ownerId'] } +export type UserProjectProfile = UserProfile & ProjectPermState + +type ProjectUniqueFinder = XOR< + { slug: string }, + XOR<{ environmentId: string }, XOR<{ repositoryId: string }, { id: string }>> +> + +const projectPermsSelect = { roles: true, members: true, everyonePerms: true, ownerId: true, id: true, locked: true, status: true } as const satisfies Prisma.ProjectSelect + +export async function authUser(req: FastifyRequest): Promise +export async function authUser(req: FastifyRequest, projectUnique: ProjectUniqueFinder): Promise +export async function authUser(req: FastifyRequest, projectUnique?: ProjectUniqueFinder): Promise { + let adminPermissions: bigint = 0n + let tokenId: string | undefined + let user: UserDetails | undefined + + if (req.session.user) { + const loginResult = await logViaSession(req.session.user) + user = { + ...loginResult.user, + groups: req.session.user.groups, + } + adminPermissions = loginResult.adminPerms + } else { + const tokenHeader = req.headers[tokenHeaderName] + if (typeof tokenHeader === 'string') { + const resultToken = await logViaToken(tokenHeader) + if (typeof resultToken === 'string') { + throw new Unauthorized401(resultToken) + } + adminPermissions = resultToken.adminPerms ?? 0n + tokenId = resultToken.user.tokenId + if (!user && resultToken.user) { + user = { ...resultToken.user, groups: [] } + } + } + } + + const baseReturnInfos = { + user, + adminPermissions, + tokenId, + } + if (!projectUnique || !user) { + return baseReturnInfos + } + let project: ProjectMinimalPerms | null | undefined + + if (projectUnique.repositoryId) { + project = (await prisma.repository.findUnique({ + where: { id: projectUnique.repositoryId }, + select: { project: { select: projectPermsSelect } }, + }))?.project + } else if (projectUnique.environmentId) { + project = (await prisma.environment.findUnique({ + where: { id: projectUnique.environmentId }, + select: { project: { select: projectPermsSelect } }, + }))?.project + } else if (projectUnique.id) { + project = uuid.test(projectUnique.id) + ? await prisma.project.findUnique({ + where: { id: projectUnique.id }, + select: projectPermsSelect, + }) + : await prisma.project.findUnique({ + where: { slug: projectUnique.id }, + select: projectPermsSelect, + }) + } else if (projectUnique.slug) { + project = await prisma.project.findFirstOrThrow({ + where: { slug: projectUnique.slug }, + select: projectPermsSelect, + }) + } + if (!project) { + return baseReturnInfos + } + + const projectPermissions = getProjectPermissions(project, user) + + return { + user, + adminPermissions, + projectPermissions, + projectId: project.id, + projectLocked: project.locked, + projectStatus: project.status, + projectOwnerId: project.ownerId, + } +} + +function getProjectPermissions(project: ProjectMinimalPerms, user: UserDetails): bigint | undefined { + if (project.ownerId === user.id) return PP.MANAGE + const member = project.members.find(member => member.userId === user.id) + if (!member) return + + const memberRoles = project.roles.filter(role => member.roleIds.includes(role.id)) + return memberRoles.reduce((acc, curr) => acc | curr.permissions, project.everyonePerms | PROJECT_PERMS.GUEST) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts new file mode 100644 index 000000000..7abcaa1aa --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest' +import { getJSDateFromUtcIso } from './date.js' + +describe('date-util', () => { + it('should return a native Date object', () => { + const date = '2022-10-11' + + const received = getJSDateFromUtcIso(date) + + expect(received.getMonth()).toBe(9) + expect(received.getFullYear()).toBe(2022) + expect(received.getDate()).toBeGreaterThan(10) + expect(received.getDate()).toBeLessThan(12) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts new file mode 100644 index 000000000..87473d262 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts @@ -0,0 +1,5 @@ +import { parseISO } from 'date-fns' + +export function getJSDateFromUtcIso(dateUtcIso: string) { + return parseISO(dateUtcIso) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts new file mode 100644 index 000000000..fc41aab75 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts @@ -0,0 +1,57 @@ +import * as dotenv from 'dotenv' + +if (process.env.DOCKER !== 'true') { + dotenv.config({ path: '.env' }) +} + +if (process.env.INTEGRATION === 'true') { + const envInteg = dotenv.config({ path: '.env.integ' }) + process.env = { + ...process.env, + ...(envInteg?.parsed ?? {}), + } +} + +// application mode +export const isDev = process.env.NODE_ENV === 'development' +export const isTest = process.env.NODE_ENV === 'test' +export const isProd = process.env.NODE_ENV === 'production' +export const isInt = process.env.INTEGRATION === 'true' +export const isCI = process.env.CI === 'true' +export const isDevSetup = process.env.DEV_SETUP === 'true' + +// app +export const port = process.env.SERVER_PORT +export const appVersion = isProd + ? (process.env.APP_VERSION ?? 'unknown') + : 'dev' + +// db +export const dbUrl = process.env.DB_URL + +// keycloak +export const sessionSecret = process.env.SESSION_SECRET +export const keycloakProtocol = process.env.KEYCLOAK_PROTOCOL +export const keycloakDomain = process.env.KEYCLOAK_DOMAIN +export const keycloakRealm = process.env.KEYCLOAK_REALM +export const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID +export const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET +export const keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI +export const adminsUserId = process.env.ADMIN_KC_USER_ID + ? process.env.ADMIN_KC_USER_ID.split(',') + : [] + +export const contactEmail = process.env.CONTACT_EMAIL ?? 'cloudpinative-relations@interieur.gouv.fr' + +// plugins +export const mockPlugins = process.env.MOCK_PLUGINS === 'true' +export const projectRootDir = process.env.PROJECTS_ROOT_DIR +export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins' +export const NODE_ENV = process.env.NODE_ENV === 'test' + ? 'test' + : process.env.NODE_ENV === 'development' + ? 'development' + : 'production' + +// server tuning +export const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT ? Number(process.env.PARALLEL_BULK_LIMIT) : 5 diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts new file mode 100644 index 000000000..0f1dd07fb --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts @@ -0,0 +1,48 @@ +export class ErrorResType { + readonly status: 400 | 401 | 403 | 404 | 422 | 500 + body: { message: string } = { message: '' } + constructor(code: 400 | 401 | 403 | 404 | 422 | 500) { + this.status = code + } +} +export class BadRequest400 extends ErrorResType { + constructor(message: string) { + super(400) + this.body.message = message ?? 'Bad Request' + } +} + +export class Unauthorized401 extends ErrorResType { + constructor(message?: string) { + super(401) + this.body.message = message ?? 'Unauthorized' + } +} + +export class Forbidden403 extends ErrorResType { + constructor(message?: string) { + super(403) + this.body.message = message ?? 'Forbidden' + } +} + +export class NotFound404 extends ErrorResType { + constructor() { + super(404) + this.body.message = 'Not Found' + } +} + +export class Unprocessable422 extends ErrorResType { + constructor(message?: string) { + super(422) + this.body.message = message ?? 'Unprocessable Entity' + } +} + +export class Internal500 extends ErrorResType { + constructor(message: string) { + super(500) + this.body.message = message + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts new file mode 100644 index 000000000..641977706 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts @@ -0,0 +1,55 @@ +import { randomUUID } from 'node:crypto' +import type { FastifyServerOptions } from 'fastify' +import type { generateOpenApi } from '@ts-rest/open-api' +import { swaggerUiPath } from '@cpn-console/shared' +import { loggerConf } from './logger.js' +import { + NODE_ENV, + appVersion, + keycloakClientId, + keycloakClientSecret, + keycloakRealm, + keycloakRedirectUri, +} from './env.js' +import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui' + +export const fastifyConf: FastifyServerOptions = { + maxParamLength: 5000, + logger: loggerConf[NODE_ENV] ?? loggerConf.production, + genReqId: () => randomUUID(), +} + +const externalDocs = { + description: 'External documentation.', + url: 'https://cloud-pi-native.fr', +} + +export const swaggerConf: Parameters[1] = { + info: { + title: 'Console Cloud Pi Native', + description: 'API de gestion des ressources Cloud Pi Native.', + version: appVersion, + }, + + externalDocs, + servers: [ + { + url: keycloakRedirectUri, + }, + ], +} + +export const swaggerUiConf: FastifySwaggerUiOptions = { + routePrefix: swaggerUiPath, + uiConfig: { + docExpansion: 'list', + deepLinking: false, + }, + initOAuth: { + clientId: keycloakClientId, + clientSecret: keycloakClientSecret, + realm: keycloakRealm, + appName: 'Cloud Pi Native', + scopes: 'openid generic', + }, +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts new file mode 100644 index 000000000..d7d1bdc3a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts @@ -0,0 +1,235 @@ +import type { KubeCluster, KubeUser, Project as ProjectPayload, Store } from '@cpn-console/hooks' +import { describe, expect, it } from 'vitest' +import type { ProjectInfos, ReposCreds } from './hook-wrapper.ts' +import { transformToHookProject } from './hook-wrapper.ts' + +const associatedCluster = { + id: 'f0e39981-0b6d-4c16-aa96-225062b75767', + infos: '', + label: 'carno', + privacy: 'dedicated', + secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9b60', + kubeconfig: { + id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1437', + user: { + keyData: 'REDACTED', + certData: 'REDACTED', + }, + cluster: { + caData: 'REDACTED', + server: 'https://api-server:6443', + skipTLSVerify: false, + tlsServerName: 'api-server', + }, + createdAt: '2024-05-02T09:17:27.882Z', + updatedAt: '2024-05-02T09:17:27.882Z', + }, + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', + slug: 'default', + }, +} +const nonAssociatedCluster = { + id: 'f0e39981-0b6d-4c16-aa96-225062b75111', + infos: '', + label: 'carno2', + privacy: 'dedicated', + secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9111', + kubeconfig: { + id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1111', + user: { + keyData: 'REDACTED', + certData: 'REDACTED', + }, + cluster: { + caData: 'REDACTED', + server: 'https://api-server:6443', + skipTLSVerify: false, + tlsServerName: 'api-server', + }, + createdAt: '2024-05-02T09:17:27.882Z', + updatedAt: '2024-05-02T09:17:27.882Z', + }, + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', + slug: 'default', + }, +} +const project: ProjectInfos = { + id: '011e7860-04d7-461f-912d-334c622d38b3', + name: 'candilib', + description: 'Application de réservation de places à l\'examen du permis B.', + status: 'created', + locked: false, + createdAt: '2023-07-03T14:46:56.778Z', + updatedAt: '2023-07-03T14:46:56.783Z', + everyonePerms: 896n, + ownerId: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', + members: [], + clusters: [associatedCluster, nonAssociatedCluster], + environments: [ + { + id: '1b9f1053-fcf5-4053-a7b2-ff8a2c0c1921', + name: 'dev', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + createdAt: '2023-07-03T14:46:56.787Z', + updatedAt: '2023-07-03T14:46:56.803Z', + clusterId: 'aaaaaaaa-5b03-45d5-847b-149dec875680', + quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', + stageId: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', + quota: { + id: '5a57b62f-2465-4fb6-a853-5a751d099199', + memory: '4Gi', + cpu: 2, + name: 'small', + isPrivate: false, + }, + stage: { + id: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', + name: 'dev', + }, + cluster: { + id: 'aaaaaaaa-5b03-45d5-847b-149dec875680', + infos: 'Floating IP : 0.0.0.0', + label: 'pas-top-cluster', + privacy: 'dedicated', + secretName: '94d52618-7869-4192-b33e-85dd0959e815', + kubeconfig: { + id: 'b5662039-a62b-483e-ba54-b12c6f966c96', + user: { + token: 'kirikou', + }, + cluster: { + server: 'https://pwned.cluster', + tlsServerName: 'pwned.cluster', + }, + createdAt: '2024-07-24T16:54:14.969Z', + updatedAt: '2024-07-24T16:54:14.969Z', + }, + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', + slug: 'pub', + }, + }, + }, + { + id: '1c654f00-4798-4a80-929f-960ddb37885a', + name: 'integration', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + createdAt: '2023-07-03T14:46:56.788Z', + updatedAt: '2023-07-03T14:46:56.803Z', + clusterId: '126ac57f-263c-4463-87bb-d4e9017056b2', + quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', + stageId: 'd434310e-7850-4d59-b47f-0772edf50582', + quota: { + id: '5a57b62f-2465-4fb6-a853-5a751d099199', + memory: '4Gi', + cpu: 2, + name: 'small', + isPrivate: false, + }, + stage: { + id: 'd434310e-7850-4d59-b47f-0772edf50582', + name: 'integration', + }, + cluster: { + id: '126ac57f-263c-4463-87bb-d4e9017056b2', + infos: null, + label: 'top-secret-cluster', + privacy: 'dedicated', + secretName: '59be2d50-58f9-42f3-95dc-b0c0518e3d8a', + kubeconfig: { + id: '0e88f000-07e6-4781-a69d-0963489387f7', + user: { + token: 'nyan cat', + }, + cluster: { + server: 'https://nothere.cluster', + skipTLSVerify: false, + tlsServerName: 'nothere.cluster', + }, + createdAt: '2024-07-24T16:54:14.966Z', + updatedAt: '2024-07-24T16:54:14.966Z', + }, + clusterResources: true, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', + slug: 'pub', + }, + }, + }, + ], + repositories: [ + { + id: '299216bb-2dcc-42b5-ac71-6aa001d2dccf', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + internalRepoName: 'candilib', + externalRepoUrl: 'https://github.com/dnum-mi/candilib.git', + externalUserName: 'this-is-a-test', + isInfra: false, + isPrivate: true, + createdAt: '2023-07-03T14:46:56.788Z', + updatedAt: '2023-07-03T14:46:56.802Z', + }, + ], + plugins: [], + owner: { + id: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', + firstName: 'Jean', + lastName: 'DUPOND', + email: 'test@test.com', + createdAt: '2023-07-03T14:46:56.770Z', + updatedAt: '2023-07-03T14:46:56.770Z', + adminRoleIds: [], + }, + roles: [], +} + +describe('transformToHookProject', () => { + // Mock data + const mockStore: Store = {} + const mockReposCreds: ReposCreds = { + console: { + token: 'test', + username: 'test', + }, + } + + it('transforme correctement le projet en objet Payload', () => { + const result: ProjectPayload = transformToHookProject(project, mockStore, mockReposCreds) + + // Asserts pour vérifier la transformation + + // Assert sur la transformation des utilisateurs + expect(result.users).toEqual([project.owner]) + + // Assert sur la transformation des rôles + expect(result.roles).toEqual([{ userId: project.owner.id, role: 'owner' }]) + + // Assert sur la transformation des clusters + expect(result.clusters).toEqual([associatedCluster, nonAssociatedCluster].map(({ kubeconfig, ...cluster }) => ({ + user: kubeconfig.user as unknown as KubeUser, + cluster: kubeconfig.cluster as unknown as KubeCluster, + ...cluster, + privacy: cluster.privacy, + }))) + + // Assert sur la transformation des environnements + expect(result.environments).toEqual(project.environments.map(({ permissions: _, stage, quota, ...environment }) => ({ + quota, + stage: stage.name, + permissions: [{ permissions: { rw: true, ro: true }, userId: project.ownerId }], + ...environment, + apis: {}, + }))) + + // Assert sur la transformation des repositories + expect(result.repositories).toEqual(project.repositories.map(repo => ({ ...repo, newCreds: mockReposCreds[repo.internalRepoName] }))) + + // Assert sur le store + expect(result.store).toEqual(mockStore) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts new file mode 100644 index 000000000..4a248450e --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts @@ -0,0 +1,231 @@ +import type { Cluster, Kubeconfig, Project, ProjectRole, Zone } from '@prisma/client' +import type { ClusterObject, HookResult, KubeCluster, KubeUser, Project as ProjectPayload, RepoCreds, Repository, Store, ZoneObject } from '@cpn-console/hooks' +import { hooks } from '@cpn-console/hooks' +import type { AsyncReturnType } from '@cpn-console/shared' +import { ProjectAuthorized, getPermsByUserRoles, resourceListToDict } from '@cpn-console/shared' +import { genericProxy } from './proxy.js' +import { archiveProject, getAdminPlugin, getClusterByIdOrThrow, getClusterNamesByZoneId, getClustersAssociatedWithProject, getHookProjectInfos, getHookRepository, getProjectStore, getZoneByIdOrThrow, saveProjectStore, updateProjectClusterHistory, updateProjectCreated, updateProjectFailed, updateProjectWarning } from '@/resources/queries-index.js' +import type { ConfigRecords } from '@/resources/project-service/business.js' +import { dbToObj } from '@/resources/project-service/business.js' + +export type ReposCreds = Record +export type ProjectInfos = AsyncReturnType + +async function getProjectPayload(projectId: Project['id'], reposCreds?: ReposCreds) { + const [ + project, + store, + clusters, + ] = await Promise.all([ + getHookProjectInfos(projectId), + getProjectStore(projectId), + getClustersAssociatedWithProject(projectId), + ]) + + return transformToHookProject({ + ...project, + clusters, + }, dbToObj(store), reposCreds) +} + +async function upsertProject(projectId: Project['id'], reposCreds?: ReposCreds) { + const [payload, config] = await Promise.all([ + getProjectPayload(projectId, reposCreds), + getAdminPlugin(), + ]) + + const results = await hooks.upsertProject.execute(payload, dbToObj(config)) + + const records: ConfigRecords = Object.entries(results.results).reduce((acc, [pluginName, result]) => { + if (result.store) { + return [...acc, ...Object.entries(result.store).map(([key, value]) => ({ pluginName, key, value: String(value) }))] + } + return acc + }, [] as ConfigRecords) + + await saveProjectStore(records, projectId) + const project = await manageProjectStatus(projectId, results, 'upsert', payload.environments.map(env => env.clusterId)) + return { + results, + project, + } +} +const project = { + upsert: async (projectId: Project['id'], reposCreds?: ReposCreds) => { + const results = await upsertProject(projectId, reposCreds) + // automatically retry one time if it fails + return results.results.failed ? upsertProject(projectId, reposCreds) : results + }, + delete: async (projectId: Project['id']) => { + const [payload, config] = await Promise.all([ + getProjectPayload(projectId), + getAdminPlugin(), + ]) + const results = await hooks.deleteProject.execute(payload, dbToObj(config)) + return { + results, + project: await manageProjectStatus(projectId, results, 'delete', []), + } + }, + getSecrets: async (projectId: Project['id']) => { + const project = await getHookProjectInfos(projectId) + const store = dbToObj(await getProjectStore(project.id)) + const config = dbToObj(await getAdminPlugin()) + + return hooks.getProjectSecrets.execute({ ...project, store }, config) + }, +} as const + +type ProjectAction = keyof typeof project +async function manageProjectStatus(projectId: Project['id'], hookReply: HookResult, action: ProjectAction, envClusterIds: Cluster['id'][]): Promise> { + if (!hookReply.failed && hookReply.results?.kubernetes) { + await updateProjectClusterHistory(projectId, envClusterIds) + } + if (hookReply.failed) { + return updateProjectFailed(projectId) + } else if (hookReply.warning.length) { + return updateProjectWarning(projectId) + } else if (action === 'upsert') { + return updateProjectCreated(projectId) + } else if (action === 'delete') { + return archiveProject(projectId) + } + throw new Error('unknown action') +} + +const cluster = { + upsert: async (clusterId: Cluster['id'], previousZoneId: Cluster['zoneId']) => { + const cluster = await getClusterByIdOrThrow(clusterId) + const clusterObject = cluster as unknown as ClusterObject + const store = dbToObj(await getAdminPlugin()) + if (cluster.zoneId !== previousZoneId) { + // Upsert on the old zone to remove cluster + const previousClusterObject = { + ...cluster, + } as unknown as ClusterObject + previousClusterObject.zone = await getZoneByIdOrThrow(previousZoneId) + previousClusterObject.zone.clusterNames = await getClusterNamesByZoneId(previousZoneId) + const hookResult = await hooks.upsertCluster.execute({ + ...cluster.kubeconfig as unknown as Pick, + ...previousClusterObject, + }, store) + if (hookResult.failed) { + return hookResult + } + } + clusterObject.zone.clusterNames = await getClusterNamesByZoneId(cluster.zoneId) + return hooks.upsertCluster.execute({ + ...cluster.kubeconfig as unknown as Pick, + ...clusterObject, + }, store) + }, + delete: async (clusterId: Cluster['id']) => { + const cluster = await getClusterByIdOrThrow(clusterId) + const clusterObject = cluster as unknown as ClusterObject + const clusterNames = await getClusterNamesByZoneId(cluster.zoneId) + clusterObject.zone.clusterNames = clusterNames.filter(c => c !== cluster.label) + const store = dbToObj(await getAdminPlugin()) + return hooks.deleteCluster.execute({ + ...cluster.kubeconfig as unknown as ClusterObject, + ...clusterObject, + }, store) + }, +} as const + +const user = { + retrieveUserByEmail: async (email: string) => { + const config = dbToObj(await getAdminPlugin()) + return hooks.retrieveUserByEmail.execute({ email }, config) + }, +} as const + +const zone = { + upsert: async (zoneId: Zone['id']) => { + const zone: ZoneObject = await getZoneByIdOrThrow(zoneId) + zone.clusterNames = await getClusterNamesByZoneId(zoneId) + const store = dbToObj(await getAdminPlugin()) + return hooks.upsertZone.execute(zone, store) + }, + delete: async (zoneId: Zone['id']) => { + const zone = await getZoneByIdOrThrow(zoneId) + const store = dbToObj(await getAdminPlugin()) + return hooks.deleteZone.execute(zone, store) + }, +} as const + +const misc = { + checkServices: async () => { + const config = dbToObj(await getAdminPlugin()) + return hooks.checkServices.execute({}, config) + }, + syncRepository: async (repoId: string, { syncAllBranches, branchName }: { syncAllBranches: boolean, branchName?: string }) => { + const { project, ...repoInfos } = await getHookRepository(repoId) + const store = dbToObj(await getProjectStore(project.id)) + const payload = { + repo: { ...repoInfos, syncAllBranches, branchName }, + ...project, + store, + } + const config = dbToObj(await getAdminPlugin()) + return hooks.syncRepository.execute(payload, config) + }, +} as const + +export const hook = { + // @ts-ignore TODO voir comment opti la signature de la fonction + misc: genericProxy(misc), + // @ts-ignore TODO voir comment opti la signature de la fonction + project: genericProxy(project, { upsert: ['delete'], delete: ['upsert', 'delete'], getSecrets: ['delete'] }), + // @ts-ignore TODO voir comment opti la signature de la fonction + cluster: genericProxy(cluster, { delete: ['upsert', 'delete'], upsert: ['delete'] }), + // @ts-ignore TODO voir comment opti la signature de la fonction + zone: genericProxy(zone, { delete: ['upsert'], upsert: ['delete'] }), + // @ts-ignore TODO voir comment opti la signature de la fonction + user: genericProxy(user, {}), +} + +function formatClusterInfos({ kubeconfig, ...cluster }: Omit + & { kubeconfig: Kubeconfig, zone: Pick }) { + return { + user: kubeconfig.user as unknown as KubeUser, + cluster: kubeconfig.cluster as unknown as KubeCluster, + ...cluster, + privacy: cluster.privacy, + } +} +export type RolesById = Record + +export function transformToHookProject(project: ProjectInfos, store: Store, reposCreds: ReposCreds = {}): ProjectPayload { + const clusters = project.clusters.map(cluster => formatClusterInfos(cluster)) + const rolesById = resourceListToDict(project.roles) + + return ({ + ...project, + clusters, + environments: project.environments.map(({ stage, ...environment }) => ({ + stage: stage.name, + permissions: [ + { permissions: { rw: true, ro: true }, userId: project.ownerId }, + ...project.members.map(member => ({ + userId: member.userId, + permissions: { + ro: ProjectAuthorized.ListEnvironments({ adminPermissions: 0n, projectPermissions: getPermsByUserRoles(member.roleIds, rolesById, project.everyonePerms) }), + rw: ProjectAuthorized.ManageEnvironments({ adminPermissions: 0n, projectPermissions: getPermsByUserRoles(member.roleIds, rolesById, project.everyonePerms) }), + }, + })), + ], + ...environment, + apis: {}, + })), + repositories: project.repositories.map(repo => ({ ...repo, newCreds: reposCreds[repo.internalRepoName] })), + store, + users: [project.owner, ...project.members.map(({ user }) => user)], + roles: [ + { userId: project.ownerId, role: 'owner' }, + ...project.members.map(member => ({ + userId: member.userId, + role: 'user' as const, + })), + ], + }) +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts new file mode 100644 index 000000000..42c9cfae0 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { userPayloadMapper } from './keycloak-utils.js' + +describe('keycloak', () => { + it('should map keycloak user object to DSO user object without groups', () => { + const payload = { + sub: 'thisIsAnId', + email: 'test@test.com', + given_name: 'Jean', + family_name: 'DUPOND', + } + const desired = { + id: 'thisIsAnId', + email: 'test@test.com', + firstName: 'Jean', + lastName: 'DUPOND', + groups: [], + } + + const transformed = userPayloadMapper(payload) + + expect(transformed).toMatchObject(desired) + }) + + it('should map keycloak user object to DSO user object with groups', () => { + const payload = { + sub: 'thisIsAnId', + email: 'test@test.com', + given_name: 'Jean', + family_name: 'DUPOND', + groups: ['group1'], + } + const desired = { + id: 'thisIsAnId', + email: 'test@test.com', + firstName: 'Jean', + lastName: 'DUPOND', + groups: ['group1'], + } + + const transformed = userPayloadMapper(payload) + + expect(transformed).toMatchObject(desired) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts new file mode 100644 index 000000000..462116029 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts @@ -0,0 +1,27 @@ +import { tokenHeaderName } from '@cpn-console/shared' +import type { FastifyRequest } from 'fastify' + +interface KeycloakPayload { + sub: string + email: string + given_name: string + family_name: string + groups: string[] +} + +export function userPayloadMapper(userPayload: KeycloakPayload) { + return { + id: userPayload.sub, + email: userPayload.email, + firstName: userPayload.given_name, + lastName: userPayload.family_name, + groups: userPayload.groups || [], + } +} + +export function bypassFn(request: FastifyRequest) { + try { + return !!request.headers[tokenHeaderName] + } catch (_e) {} + return false +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts new file mode 100644 index 000000000..1d0159d40 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts @@ -0,0 +1,42 @@ +import { serviceContract, swaggerUiPath, systemContract } from '@cpn-console/shared' +import type { KeycloakOptions } from 'fastify-keycloak-adapter' +import { + keycloakClientId, + keycloakClientSecret, + keycloakDomain, + keycloakProtocol, + keycloakRealm, + keycloakRedirectUri, + sessionSecret, +} from './env.js' +import { bypassFn, userPayloadMapper } from './keycloak-utils.js' + +export const keycloakConf = { + appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', + keycloakSubdomain: `${keycloakDomain}/realms/${keycloakRealm}`, + clientId: keycloakClientId ?? '', + clientSecret: keycloakClientSecret ?? '', + useHttps: keycloakProtocol === 'https', + disableCookiePlugin: true, + disableSessionPlugin: true, + // @ts-ignore + userPayloadMapper, + retries: 5, + excludedPatterns: [ + systemContract.getVersion.path, + systemContract.getHealth.path, + serviceContract.getServiceHealth.path, + `${swaggerUiPath}/**`, + ], + bypassFn, +} as const satisfies KeycloakOptions + +export const sessionConf = { + cookieName: 'sessionId', + secret: sessionSecret || 'a-very-strong-secret-with-more-than-32-char', + cookie: { + httpOnly: true, + secure: true, + }, + expires: 1_800_000, +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts new file mode 100644 index 000000000..4857696ae --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts @@ -0,0 +1,97 @@ +import type { FastifyBaseLogger, FastifyLogFn, PinoLoggerOptions } from 'fastify/types/logger.js' +import type { XOR } from '@cpn-console/shared' +import { logger as customLogger } from '@/app.js' + +export const customLevels = { + audit: 25, +} + +export const loggerConf: Record = { + development: { + transport: { + target: 'pino-pretty', + options: { + translateTime: 'dd/mm/yyyy - HH:MM:ss Z', + ignore: 'pid,hostname', + colorize: true, + singleLine: true, + }, + }, + customLevels, + level: process.env.LOG_LEVEL ?? 'debug', + }, + production: { + customLevels, + level: process.env.LOG_LEVEL ?? 'audit', + }, + test: { + level: 'silent', + }, +} + +type LoggerType = 'info' | 'warn' | 'error' | 'fatal' | 'trace' | 'debug' | 'audit' | undefined +const loggerWrapper = { + level: '', + child: () => loggerWrapper, + silent: () => {}, + audit: (msg: string | unknown) => console.log(msg), + info: (msg: string | unknown) => console.log(msg), + warn: (msg: string | unknown) => console.warn(msg), + error: (msg: string | unknown) => console.error(msg), + fatal: (msg: string | unknown) => console.error(msg), + trace: (msg: string | unknown) => console.trace(msg), + debug: (msg: string | unknown) => console.debug(msg), +} + +export function log( + type: LoggerType, + { + reqId, + userId, + tokenId, + message, + error, + infos, + }: { + reqId?: string + userId?: string + tokenId?: string + infos?: Record + } & XOR<{ message: string }, { error: Record | string | Error }>, +) { + const logger = customLogger || loggerWrapper + + const logInfos = { + message, + infos, + reqId, + userId, + tokenId, + } + + if (error) { + const errorInfos = { + ...logInfos, + error: { + message: typeof error === 'string' ? error : error?.message || 'unexpected error', + trace: error instanceof Error && error?.stack, + }, + } + logger.error({ ...errorInfos }) + return + } + logger[type || 'info']({ reqId, userId, logInfos }) +} + +export interface CustomLogger extends FastifyBaseLogger { + /** + * Log at `'audit'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. + * If more args follows `msg`, these will be used to format `msg` using `util.format`. + * + * @typeParam T: the interface of the object being serialized. Default is object. + * @param obj: object to be serialized + * @param msg: the log message to write + * @param ...args: format string values when `msg` is a format string + */ + audit: FastifyLogFn +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts new file mode 100644 index 000000000..90f04ecbc --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts @@ -0,0 +1,152 @@ +import fp from 'fastify-plugin' +import type { Repository } from '@prisma/client' +import type { PluginsManifests, RepoCreds, ServiceInfos } from '@cpn-console/hooks' +import { editStrippers, populatePluginManifests } from '@cpn-console/hooks' +import { DEFAULT, DISABLED, PROJECT_PERMS } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import type { UserDetails } from '../types/index.js' +import type * as utilsController from '../utils/controller.js' + +let requestor: Requestor + +export function setRequestor(user: Requestor = getRandomRequestor()) { + requestor = user +} + +export function getRequestor() { + return requestor +} + +export async function mockSessionPlugin() { + const sessionPlugin = (app, opt, next) => { + app.addHook('onRequest', (req, res, next) => { + req.session = { user: getRequestor() } + next() + }) + next() + } + + return { default: fp(sessionPlugin) } +} + +export async function mockHooksPackage() { + const hookTemplate = { + execute: () => ({ + args: {}, + failed: false, + }), + validate: () => ({ + failed: false, + }), + } + + return { + editStrippers, + populatePluginManifests, + services: { + getStatus: () => [], + refreshStatus: async () => [], + }, + PluginApi: class { }, + servicesInfos: { + registry: { title: 'Harbor', name: 'registry', to: () => 'test' }, + plugin2: { title: 'Plugin2', name: 'plugin2', to: () => ({ to: 'test', title: 'Test' }) }, + plugin3: { title: 'Plugin3', name: 'plugin3', to: () => [{ to: 'test', title: 'Test' }] }, + plugin4: { title: 'Plugin4', name: 'plugin4', to: () => [{ to: 'test' }] }, + plugin5: { title: 'Plugin5', name: 'plugin5' }, + } as Record, + pluginsManifests: { + registry: { + title: 'Harbor', + global: [{ + kind: 'switch', + initialValue: DEFAULT, + key: 'test2', + permissions: { + admin: { read: true, write: true }, + user: { read: true, write: false }, + }, + title: 'Test2', + value: DEFAULT, + description: 'description', + }], + project: [{ + kind: 'switch', + key: 'test2', + permissions: { + admin: { read: true, write: true }, + user: { read: true, write: true }, + }, + title: 'Test', + value: DEFAULT, + initialValue: DISABLED, + }], + }, + } as PluginsManifests, + hooks: { + // projects + getProjectSecrets: { + execute: () => ({ + failed: false, + args: {}, + results: { + registry: { + secrets: { + token: 'myToken', + }, + status: { + failed: false, + }, + }, + }, + }), + }, + upsertProject: hookTemplate, + deleteProject: hookTemplate, + // clusters + upsertCluster: hookTemplate, + deleteCluster: hookTemplate, + // user + retrieveUserByEmail: hookTemplate, + }, + } +} + +export type ReposCreds = Record + +type Requestor = Partial +export function getRandomRequestor(user?: Requestor): Partial { + return { + id: user?.id ?? faker.string.uuid(), + email: user?.email ?? faker.internet.email(), + firstName: user?.firstName ?? faker.person.firstName(), + lastName: user?.lastName ?? faker.person.lastName(), + type: 'human', + ...user?.groups !== null && { groups: user?.groups ?? [] }, + } +} + +export function getUserMockInfos(isAdmin: boolean, user?: UserDetails): utilsController.UserProfile & utilsController.ProjectPermState +export function getUserMockInfos(isAdmin: boolean, user?: UserDetails, project?: utilsController.ProjectPermState): utilsController.UserProjectProfile & utilsController.ProjectPermState +export function getUserMockInfos(isAdmin: boolean, user = getRandomRequestor(), project?: utilsController.ProjectPermState): utilsController.UserProfile | utilsController.UserProjectProfile { + return { + adminPermissions: isAdmin ? 2n : 0n, + user, + ...project, + } +} + +export function getProjectMockInfos({ projectId, projectLocked, projectOwnerId, projectPermissions, projectStatus }: Partial): utilsController.ProjectPermState { + return { + projectId: projectId ?? faker.string.uuid(), + projectLocked: projectLocked ?? false, + projectOwnerId: projectOwnerId ?? faker.string.uuid(), + projectStatus: projectStatus ?? 'created', + projectPermissions: projectPermissions ?? PROJECT_PERMS.MANAGE, + } +} + +export const atDates = { + createdAt: new Date(), + updatedAt: new Date(), +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts new file mode 100644 index 000000000..987667776 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts @@ -0,0 +1,9 @@ +import type { PluginManagerOptions } from '@cpn-console/hooks' +import { isCI, isInt, isProd } from './env.js' + +export const pluginManagerOptions: PluginManagerOptions = { + mockHooks: isCI || (!isProd && !isInt), + mockMonitoring: isCI || (!isProd && !isInt), + mockExternalServices: isCI || (!isProd && !isInt), + startPlugins: (!isCI && isProd) || isInt, +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts new file mode 100644 index 000000000..a5e0cd9f4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from 'vitest' +import { genericProxy } from './proxy.js' + +// Création d'une cible de test +const target = { + async fetchData(id: string) { + return { id, data: 'Mocked data' } + }, + async otherMethod(id: string) { + return { id, data: 'Mocked data' } + }, +} + +describe('test calls without ID passed', () => { + // Test d'appel de méthode sans ID + it('calling method without ID', async () => { + const proxied = genericProxy(target) + const result = await proxied.fetchData() + expect(result).toEqual({ id: undefined, data: 'Mocked data' }) + }) + + // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie + it('test when no ID is provided', async () => { + // Création d'une cible de test + const target = { + async fetchData() { + return 'No ID provided' + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + // Appel à la méthode fetchData sans ID + const result = await proxied.fetchData() + + // Vérification que le résultat est correct + expect(result).toBe('No ID provided') + }) + + // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie avec une promesse en cours + it('test when no ID is provided with pending promise', async () => { + // Création d'une cible de test + const target = { + async fetchData() { + return new Promise(resolve => setTimeout(() => resolve('Pending result'), 100)) + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + // Appel à la méthode fetchData sans ID + const promise1 = proxied.fetchData() + const promise2 = proxied.fetchData() // Deuxième appel avant la résolution du premier + + // Attendre que la première promesse se résolve + const result1 = await promise1 + + // Vérification que le résultat de la première promesse est correct + expect(result1).toBe('Pending result') + + // Attendre que la deuxième promesse se résolve + const result2 = await promise2 + + // Vérification que le résultat de la deuxième promesse est correct + expect(result2).toBe('Pending result') + }) + // Test pour vérifier que l'erreur est levée lorsque args est fourni sans ID + it('test error when args provided without ID', async () => { + // Création d'une cible de test + const target = { + async fetchData(_id: string, _args: any) { + return 'No ID provided' + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + const args = { key: 'value' } + + // Appel de la fonction fetchData avec des arguments mais sans ID + await expect(proxied.fetchData(undefined, args)).rejects.toThrow('ID is required when args are provided') + }) +}) + +describe('test calls with ID passed', () => { + // Test d'appel de méthode avec ID + it('calling method with ID', async () => { + const proxied = genericProxy(target) + const result = await proxied.fetchData('123') + expect(result).toEqual({ id: '123', data: 'Mocked data' }) + }) + + // Test d'appel de méthode avec exclusion en cours + it('calling method with exclusion in progress', async () => { + const proxied = genericProxy(target, { fetchData: ['otherMethod'] }) + // Simuler une exécution en cours pour la méthode exclue + proxied.otherMethod('456') + + // Maintenant, tenter d'appeler fetchData pour le même ID devrait échouer + await expect(proxied.fetchData('456')).rejects.toThrow( + 'otherMethod in progress on 456, can\'t fetchData', + ) + }) + + // Fonction de test asynchrone pour tester le mélange des nextArgs + it('test mixing nextArgs from concurrent promises', async () => { + // Création d'une cible de test + const target = { + async fetchData(id: string, args?: object) { + return { id, args } + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + const promise1 = proxied.fetchData('123', { key1: 'value1' }) + // Appels successifs à fetchData avec différents arguments + const promise2 = proxied.fetchData('123', { key2: 'value2' }) + + // Promesse concurrente avec des nextArgs différents + const promise3 = proxied.fetchData('123', { key3: 'value3' }) + + // Attendre que les promesses se résolvent + const result1 = await promise1 + const result2 = await promise2 + const result3 = await promise3 + + // Vérification que les nextArgs de promise2 et promise3 ont été correctement mélangés + expect(result1.args).toEqual({ key1: 'value1' }) + expect(result2.args).toEqual({ key2: 'value2', key3: 'value3' }) + expect(result3.args).toEqual({ key2: 'value2', key3: 'value3' }) + }) + + it('test rejection of set attempt', () => { + // Création d'une cible de test + const target = { + async fetchData() { + return 'Mocked data' + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + // Tentative de définir une nouvelle propriété sur le proxy + const setAttempt = () => { + proxied.fetchData = () => new Promise(resolve => resolve('illegal')) + } + + // Vérification que la tentative de set est rejetée + expect(setAttempt).toThrow(TypeError) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts new file mode 100644 index 000000000..ef915a7d1 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts @@ -0,0 +1,78 @@ +// @ts-nocheck un enfer à typer, pour plus tard +type Tracker> = Record + nextArgs?: [string] +}>> | Promise + +type Target = Record Promise> +type Excludes = Partial>> | undefined +const toTarget = (target: T) => ({ tracker: {} as Tracker, methods: target }) + +// @ts-ignore +export function genericProxy(proxied: T, excludes: Excludes = {}): T { + return new Proxy(toTarget(proxied), { + get({ methods, tracker }, property: string) { + if (!(property in methods)) return + return async (...args) => { + const id = args[0] as string + + if (!id && args.length > 0) { + throw new Error('ID is required when args are provided') + } + + if (!id) { + if (tracker[property] instanceof Promise) { + return tracker[property] + } + const p = methods[property]() + if (p instanceof Promise) { + tracker[property] = p + p.then(() => { + delete tracker[property] + }) + } + return p + } + if (!tracker[property]) { + tracker[property] = {} + } + + for (const testExclude of excludes[property] ?? []) { + // @ts-ignore + if (tracker?.[testExclude]?.[id]?.currentExec) { + throw new Error(`${String(testExclude)} in progress on ${id}, can't ${String(property)}`) + } + } + + if (id in tracker[property]) { + if (args[1]) { + tracker[property][id].nextArgs = { + ...(tracker[property][id].nextArgs ?? {}), + ...args[1], + } + } + if (tracker[property][id].currentExec) { + return new Promise((resolve) => { + tracker[property][id].currentExec.then(() => { + resolve(tracker[property][id].currentExec ?? methods[property](id, tracker[property][id].nextArgs)) + }) + }) + } + } + const p = methods[property](...args) + tracker[property][id] = { + currentExec: p, + nextArgs: undefined, + } + tracker[property][id].currentExec = p + p.then(() => { + tracker[property][id].currentExec = undefined + }) + return p + } + }, + set() { + return false + }, + }) as T +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts new file mode 100644 index 000000000..49580d2b5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' +import { exclude } from '@cpn-console/shared' +import { filterObjectByKeys } from './queries-tools.js' + +describe('queries-tools', () => { + it('should return a filtered object (filterObjectByKeys)', () => { + const initial = { + id: 'thisIsAnId', + name: 'alsoKeepThisKey', + description: 'keepThisKey', + } + const desired = { + name: 'alsoKeepThisKey', + description: 'keepThisKey', + } + + const transformed = filterObjectByKeys(initial, ['name', 'description']) + + expect(transformed).toMatchObject(desired) + }) + + it('should return a filtered object (exclude)', () => { + const initial = { + id: 'thisIsAnId', + name: 'myProjectName', + environment: { + permissions: { + password: 'secret', + id: 'notSecret', + }, + }, + } + const desired = { + id: 'thisIsAnId', + name: 'myProjectName', + environment: { + permissions: { + id: 'notSecret', + }, + }, + } + + const transformed = exclude(initial, ['password']) + + expect(transformed).toMatchObject(desired) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts new file mode 100644 index 000000000..856ca277f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts @@ -0,0 +1,11 @@ +export const dbKeysExcluded = ['updatedAt', 'createdAt'] + +// TODO +// @ts-ignore supprimer cette fonction et utiliser des schémas zod où elle est utilisé +export function filterObjectByKeys(obj, keys) { + return Object.fromEntries( + Object.entries(obj)?.filter(([key, _value]) => keys.includes(key)), + ) +} + +export const uuid: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts new file mode 100644 index 000000000..f754da343 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts @@ -0,0 +1,148 @@ +import { describe, expect, it } from 'vitest' +import { createRandomDbSetup } from '@cpn-console/test-utils' + +describe('random utils', () => { + // TODO + it.skip('should create a random db for tests', () => { + const db = createRandomDbSetup({ nbUsers: 3, nbRepo: 1, envs: ['dev', 'prod'] }) + expect(db).toEqual( + expect.objectContaining({ + stages: expect.arrayContaining([ + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + ]), + quotas: expect.arrayContaining([ + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + ]), + project: expect.objectContaining({ + id: expect.any(String), + name: expect.any(String), + clusters: expect.arrayContaining([{ + caData: expect.any(String), + server: expect.any(String), + tlsServername: expect.any(String), + }]), + status: expect.any(String), + locked: expect.any(Boolean), + roles: expect.arrayContaining([ + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + ]), + repositories: expect.any(Array), + environments: expect.arrayContaining([ + { + id: expect.any(String), + stageId: expect.any(String), + projectId: expect.any(String), + quotaId: expect.any(String), + status: expect.any(String), + permissions: expect.any(Array), + clusters: expect.any(Array), + }, + { + id: expect.any(String), + stageId: expect.any(String), + projectId: expect.any(String), + quotaId: expect.any(String), + status: expect.any(String), + permissions: expect.any(Array), + clusters: expect.any(Array), + }, + ]), + }), + users: expect.arrayContaining([ + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + ]), + }), + ) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/tsconfig.json b/apps/server-nestjs/src/cpin-module/old-server/tsconfig.json new file mode 100644 index 000000000..a3397898c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": [ + "@cpn-console/ts-config/tsconfig.base.json" + ], + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./src", + "paths": { + "@/*": ["src/*"] + }, + "useUnknownInCatchVariables": false, + "declarationDir": "./types", + "outDir": "./dist", + "plugins": [{ "transform": "typescript-transform-paths" }] + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "./src/**/*.spec.ts", + "./src/**/__mocks__", + "./src/mocks/utils.ts", + "./src/utils/mocks.ts" + ] +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts new file mode 100644 index 000000000..e34cc8f52 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts @@ -0,0 +1,18 @@ +/// +import { URL, fileURLToPath } from 'node:url' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + test: { + poolMatchGlobs: [ + ['**/resources/**/*.spec.ts', 'forks'], + ], + }, +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts new file mode 100644 index 000000000..596420f7d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts @@ -0,0 +1,11 @@ +process.env.ARGOCD_URL = 'https://argo-cd.readthedocs.io' +process.env.GITLAB_URL = 'https://gitlab.com' +process.env.HARBOR_URL = 'https://goharbor.io' +process.env.NEXUS_URL = 'https://sonatype.com/products/nexus-repository' +process.env.SONARQUBE_URL = 'https://www.sonarqube.org' +process.env.VAULT_URL = 'https://www.vaultproject.io' +process.env.PROJECTS_ROOT_DIR = 'forge-mi/projects' +process.env.KEYCLOAK_REDIRECT_URI = 'http://console.dso.local' +process.env.CONTACT_EMAIL = 'cloudpinative-relations@interieur.gouv.fr' +process.env.OPENCDS_URL = 'https://opencds.gouv.fr' +process.env.OPENCDS_API_TOKEN = 'test_token' diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts new file mode 100644 index 000000000..deb1bfe64 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { mergeConfig } from 'vite' +import { configDefaults, defineConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + reporters: ['default', 'hanging-process'], + environment: 'node', + testTimeout: 2000, + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**'], + exclude: [ + '**/types', + '**/mocks', + '**/*.spec.ts', + '**/*.d.ts', + '**/*.vue', + '**/queries.ts', + '**/mocks.ts', + ], + }, + include: ['src/**/*.spec.{ts,js}'], + exclude: [...configDefaults.exclude, 'e2e/*'], + setupFiles: ['./vitest-init.ts'], + root: fileURLToPath(new URL('./', import.meta.url)), + pool: 'forks', + }, + }), +) diff --git a/apps/server-nestjs/tsconfig.json b/apps/server-nestjs/tsconfig.json index aba29b0e7..fc17a4bf6 100644 --- a/apps/server-nestjs/tsconfig.json +++ b/apps/server-nestjs/tsconfig.json @@ -1,25 +1,26 @@ { "compilerOptions": { - "module": "nodenext", - "moduleResolution": "nodenext", - "resolvePackageJsonExports": true, - "esModuleInterop": true, - "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "baseUrl": "./", "declaration": true, - "removeComments": true, "emitDecoratorMetadata": true, + "esModuleInterop": true, "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2023", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, "forceConsistentCasingInFileNames": true, + "incremental": true, + "isolatedModules": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "noFallthroughCasesInSwitch": false, "noImplicitAny": false, + "outDir": "./dist", + "paths": { "@old-server/*": ["src/cpin-module/old-server/src/*"] }, + "removeComments": true, + "resolvePackageJsonExports": true, + "skipLibCheck": true, + "sourceMap": true, "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false + "strictNullChecks": true, + "target": "ES2023" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 933c18333..2424aa238 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -368,6 +368,63 @@ importers: apps/server-nestjs: dependencies: + '@cpn-console/argocd-plugin': + specifier: workspace:^ + version: file:plugins/argocd(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/gitlab-plugin': + specifier: workspace:^ + version: file:plugins/gitlab(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/harbor-plugin': + specifier: workspace:^ + version: file:plugins/harbor(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/hooks': + specifier: workspace:^ + version: file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/keycloak-plugin': + specifier: workspace:^ + version: file:plugins/keycloak(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/kubernetes-plugin': + specifier: workspace:^ + version: file:plugins/kubernetes(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/nexus-plugin': + specifier: workspace:^ + version: file:plugins/nexus(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': + specifier: workspace:^ + version: file:packages/shared(@types/node@22.19.3) + '@cpn-console/sonarqube-plugin': + specifier: workspace:^ + version: file:plugins/sonarqube(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/vault-plugin': + specifier: workspace:^ + version: file:plugins/vault(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@fastify/cookie': + specifier: ^9.4.0 + version: 9.4.0 + '@fastify/helmet': + specifier: ^11.1.1 + version: 11.1.1 + '@fastify/session': + specifier: ^10.9.0 + version: 10.9.0 + '@fastify/swagger': + specifier: ^8.15.0 + version: 8.15.0 + '@fastify/swagger-ui': + specifier: ^4.2.0 + version: 4.2.0 + '@gitbeaker/core': + specifier: ^40.6.0 + version: 40.6.0 + '@gitbeaker/rest': + specifier: ^40.6.0 + version: 40.6.0 + '@kubernetes-models/argo-cd': + specifier: ^2.6.2 + version: 2.7.2 + '@kubernetes/client-node': + specifier: ^0.22.3 + version: 0.22.3 '@nestjs/common': specifier: ^11.0.1 version: 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -377,19 +434,73 @@ importers: '@nestjs/platform-express': specifier: ^11.0.1 version: 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + '@prisma/client': + specifier: ^6.0.1 + version: 6.19.0(prisma@6.19.0(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3) + '@ts-rest/core': + specifier: ^3.52.1 + version: 3.52.1(@types/node@22.19.3)(zod@3.25.76) + '@ts-rest/fastify': + specifier: ^3.52.1 + version: 3.52.1(@ts-rest/core@3.52.1(@types/node@22.19.3)(zod@3.25.76))(fastify@4.29.1)(zod@3.25.76) + '@ts-rest/open-api': + specifier: ^3.52.1 + version: 3.52.1(@ts-rest/core@3.52.1(@types/node@22.19.3)(zod@3.25.76))(zod@3.25.76) + axios: + specifier: 1.12.2 + version: 1.12.2 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + fastify: + specifier: ^4.29.1 + version: 4.29.1 + fastify-keycloak-adapter: + specifier: 2.3.2 + version: 2.3.2(patch_hash=6846b953fc520dd1ca6cb2e790cf190cbc3ed9fa9ff69739100458c520293447) + json-2-csv: + specifier: ^5.5.7 + version: 5.5.10 + mustache: + specifier: ^4.2.0 + version: 4.2.0 + prisma: + specifier: ^6.0.1 + version: 6.19.0(magicast@0.3.5)(typescript@5.9.3) reflect-metadata: specifier: ^0.2.2 version: 0.2.2 rxjs: specifier: ^7.8.1 version: 7.8.2 + undici: + specifier: ^7.1.0 + version: 7.16.0 + vitest-mock-extended: + specifier: ^2.0.2 + version: 2.0.2(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) devDependencies: + '@cpn-console/eslint-config': + specifier: workspace:^ + version: link:../../packages/eslintconfig + '@cpn-console/test-utils': + specifier: workspace:^ + version: file:packages/test-utils(@types/node@22.19.3) + '@cpn-console/ts-config': + specifier: workspace:^ + version: link:../../packages/tsconfig '@eslint/eslintrc': specifier: ^3.2.0 version: 3.3.1 '@eslint/js': specifier: ^9.18.0 version: 9.39.1 + '@faker-js/faker': + specifier: ^9.3.0 + version: 9.9.0 '@nestjs/cli': specifier: ^11.0.0 version: 11.0.14(@types/node@22.19.3) @@ -399,6 +510,9 @@ importers: '@nestjs/testing': specifier: ^11.0.1 version: 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11) + '@trivago/prettier-plugin-sort-imports': + specifier: ^6.0.0 + version: 6.0.1(@vue/compiler-sfc@3.5.23)(prettier@3.7.4) '@types/express': specifier: ^5.0.0 version: 5.0.6 @@ -411,6 +525,9 @@ importers: '@types/supertest': specifier: ^6.0.2 version: 6.0.3 + '@vitest/coverage-v8': + specifier: ^2.1.8 + version: 2.1.9(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) eslint: specifier: ^9.18.0 version: 9.39.1(jiti@2.6.1) @@ -420,15 +537,27 @@ importers: eslint-plugin-prettier: specifier: ^5.2.2 version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4) + fastify-plugin: + specifier: ^5.0.1 + version: 5.1.0 globals: specifier: ^16.0.0 version: 16.5.0 jest: specifier: ^30.0.0 version: 30.2.0(@types/node@22.19.3)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + nodemon: + specifier: ^3.1.7 + version: 3.1.10 + pino-pretty: + specifier: ^13.0.0 + version: 13.1.2 prettier: specifier: ^3.4.2 version: 3.7.4 + rimraf: + specifier: ^6.0.1 + version: 6.1.0 source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -444,6 +573,9 @@ importers: ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.3)(typescript@5.9.3) + ts-patch: + specifier: ^3.3.0 + version: 3.3.0 tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -453,6 +585,18 @@ importers: typescript-eslint: specifier: ^8.20.0 version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + typescript-transform-paths: + specifier: ^3.5.2 + version: 3.5.5(typescript@5.9.3) + vite: + specifier: ^7.2.1 + version: 7.2.1(@types/node@22.19.3)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1) + vite-node: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.3)(terser@5.44.1) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1) packages/eslintconfig: devDependencies: @@ -1847,6 +1991,39 @@ packages: resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} engines: {node: '>=v18'} + '@cpn-console/argocd-plugin@file:plugins/argocd': + resolution: {directory: plugins/argocd, type: directory} + + '@cpn-console/gitlab-plugin@file:plugins/gitlab': + resolution: {directory: plugins/gitlab, type: directory} + + '@cpn-console/harbor-plugin@file:plugins/harbor': + resolution: {directory: plugins/harbor, type: directory} + + '@cpn-console/hooks@file:packages/hooks': + resolution: {directory: packages/hooks, type: directory} + + '@cpn-console/keycloak-plugin@file:plugins/keycloak': + resolution: {directory: plugins/keycloak, type: directory} + + '@cpn-console/kubernetes-plugin@file:plugins/kubernetes': + resolution: {directory: plugins/kubernetes, type: directory} + + '@cpn-console/nexus-plugin@file:plugins/nexus': + resolution: {directory: plugins/nexus, type: directory} + + '@cpn-console/shared@file:packages/shared': + resolution: {directory: packages/shared, type: directory} + + '@cpn-console/sonarqube-plugin@file:plugins/sonarqube': + resolution: {directory: plugins/sonarqube, type: directory} + + '@cpn-console/test-utils@file:packages/test-utils': + resolution: {directory: packages/test-utils, type: directory} + + '@cpn-console/vault-plugin@file:plugins/vault': + resolution: {directory: plugins/vault, type: directory} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -3067,6 +3244,25 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@trivago/prettier-plugin-sort-imports@6.0.1': + resolution: {integrity: sha512-6B13DCWDfAfh4AEJ43gRgeCSAQmlKG5LHqHzHc0lbUwgBy0rX7o41US+46Fd4XiXBx+JDGEz3NBadCbUls0dUQ==} + engines: {node: '>= 20'} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + prettier-plugin-ember-template-tag: '>= 2.0.0' + prettier-plugin-svelte: 3.x + svelte: 4.x || 5.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + prettier-plugin-ember-template-tag: + optional: true + prettier-plugin-svelte: + optional: true + svelte: + optional: true + '@ts-rest/core@3.52.1': resolution: {integrity: sha512-tAjz7Kxq/grJodcTA1Anop4AVRDlD40fkksEV5Mmal88VoZeRKAG8oMHsDwdwPZz+B/zgnz0q2sF+cm5M7Bc7g==} peerDependencies: @@ -5960,6 +6156,9 @@ packages: engines: {node: '>=10'} hasBin: true + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + javascript-time-ago@2.5.12: resolution: {integrity: sha512-s8PPq2HQ3HIbSU0SjhNvTitf5VoXbQWof9q6k3gIX7F2il0ptjD5lONTDccpuKt/2U7RjbCp/TCHPK7eDwO7zQ==} @@ -6330,6 +6529,9 @@ packages: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash-es@4.17.22: + resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -9872,7 +10074,7 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.7 + '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@babel/traverse@7.27.7': @@ -10053,6 +10255,166 @@ snapshots: '@types/conventional-commits-parser': 5.0.2 chalk: 5.6.2 + '@cpn-console/argocd-plugin@file:plugins/argocd(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/gitlab-plugin': file:plugins/gitlab(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/hooks': file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/keycloak-plugin': file:plugins/keycloak(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/kubernetes-plugin': file:plugins/kubernetes(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@cpn-console/vault-plugin': file:plugins/vault(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@himenon/argocd-typescript-openapi': 1.2.2 + '@kubernetes-models/argo-cd': 2.7.2 + '@kubernetes/client-node': 0.22.3 + '@types/js-yaml': 4.0.9 + axios: 1.12.2 + js-yaml: 4.1.0 + kubernetes-models: 4.5.1 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - debug + - typescript + - utf-8-validate + - vitest + + '@cpn-console/gitlab-plugin@file:plugins/gitlab(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/hooks': file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@cpn-console/vault-plugin': file:plugins/vault(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@gitbeaker/core': 40.6.0 + '@gitbeaker/requester-utils': 40.6.0 + '@gitbeaker/rest': 40.6.0 + axios: 1.12.2 + js-yaml: 4.1.0 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - debug + - typescript + - utf-8-validate + - vitest + + '@cpn-console/harbor-plugin@file:plugins/harbor(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/hooks': file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/keycloak-plugin': file:plugins/keycloak(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/kubernetes-plugin': file:plugins/kubernetes(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@cpn-console/vault-plugin': file:plugins/vault(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + axios: 1.12.2 + bytes: 3.1.2 + cron-validator: 1.4.0 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - debug + - typescript + - utf-8-validate + - vitest + + '@cpn-console/hooks@file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + json-schema: 0.4.0 + vitest-mock-extended: 2.0.2(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + zod: 3.25.76 + transitivePeerDependencies: + - '@types/node' + - typescript + - vitest + + '@cpn-console/keycloak-plugin@file:plugins/keycloak(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/hooks': file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@keycloak/keycloak-admin-client': 26.4.2 + axios: 1.12.2 + transitivePeerDependencies: + - '@types/node' + - debug + - typescript + - vitest + + '@cpn-console/kubernetes-plugin@file:plugins/kubernetes(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/hooks': file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@kubernetes-models/argo-cd': 2.7.2 + '@kubernetes/client-node': 0.22.3 + axios: 1.12.2 + kubernetes-models: 4.5.1 + request: 2.88.2 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - debug + - typescript + - utf-8-validate + - vitest + + '@cpn-console/nexus-plugin@file:plugins/nexus(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/gitlab-plugin': file:plugins/gitlab(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/hooks': file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@cpn-console/vault-plugin': file:plugins/vault(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + axios: 1.12.2 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - debug + - typescript + - utf-8-validate + - vitest + + '@cpn-console/shared@file:packages/shared(@types/node@22.19.3)': + dependencies: + '@ts-rest/core': 3.52.1(@types/node@22.19.3)(zod@3.25.76) + short-uuid: 5.2.0 + zod: 3.25.76 + zod-validation-error: 3.5.4(zod@3.25.76) + transitivePeerDependencies: + - '@types/node' + + '@cpn-console/sonarqube-plugin@file:plugins/sonarqube(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/gitlab-plugin': file:plugins/gitlab(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/hooks': file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/keycloak-plugin': file:plugins/keycloak(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@cpn-console/vault-plugin': file:plugins/vault(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + axios: 1.12.2 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - debug + - typescript + - utf-8-validate + - vitest + + '@cpn-console/test-utils@file:packages/test-utils(@types/node@22.19.3)': + dependencies: + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@faker-js/faker': 9.9.0 + transitivePeerDependencies: + - '@types/node' + + '@cpn-console/vault-plugin@file:plugins/vault(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@cpn-console/hooks': file:packages/hooks(@types/node@22.19.3)(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)) + '@cpn-console/shared': file:packages/shared(@types/node@22.19.3) + '@kubernetes/client-node': 0.22.3 + axios: 1.12.2 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - debug + - typescript + - utf-8-validate + - vitest + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -11305,11 +11667,39 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@trivago/prettier-plugin-sort-imports@6.0.1(@vue/compiler-sfc@3.5.23)(prettier@3.7.4)': + dependencies: + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + javascript-natural-sort: 0.7.1 + lodash-es: 4.17.22 + minimatch: 9.0.5 + parse-imports-exports: 0.2.4 + prettier: 3.7.4 + optionalDependencies: + '@vue/compiler-sfc': 3.5.23 + transitivePeerDependencies: + - supports-color + + '@ts-rest/core@3.52.1(@types/node@22.19.3)(zod@3.25.76)': + optionalDependencies: + '@types/node': 22.19.3 + zod: 3.25.76 + '@ts-rest/core@3.52.1(@types/node@24.10.0)(zod@3.25.76)': optionalDependencies: '@types/node': 24.10.0 zod: 3.25.76 + '@ts-rest/fastify@3.52.1(@ts-rest/core@3.52.1(@types/node@22.19.3)(zod@3.25.76))(fastify@4.29.1)(zod@3.25.76)': + dependencies: + '@ts-rest/core': 3.52.1(@types/node@22.19.3)(zod@3.25.76) + fastify: 4.29.1 + optionalDependencies: + zod: 3.25.76 + '@ts-rest/fastify@3.52.1(@ts-rest/core@3.52.1(@types/node@24.10.0)(zod@3.25.76))(fastify@4.29.1)(zod@3.25.76)': dependencies: '@ts-rest/core': 3.52.1(@types/node@24.10.0)(zod@3.25.76) @@ -11317,6 +11707,13 @@ snapshots: optionalDependencies: zod: 3.25.76 + '@ts-rest/open-api@3.52.1(@ts-rest/core@3.52.1(@types/node@22.19.3)(zod@3.25.76))(zod@3.25.76)': + dependencies: + '@anatine/zod-openapi': 1.14.2(openapi3-ts@2.0.2)(zod@3.25.76) + '@ts-rest/core': 3.52.1(@types/node@22.19.3)(zod@3.25.76) + openapi3-ts: 2.0.2 + zod: 3.25.76 + '@ts-rest/open-api@3.52.1(@ts-rest/core@3.52.1(@types/node@24.10.0)(zod@3.25.76))(zod@3.25.76)': dependencies: '@anatine/zod-openapi': 1.14.2(openapi3-ts@2.0.2)(zod@3.25.76) @@ -11830,6 +12227,24 @@ snapshots: vite: 7.2.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1) vue: 3.5.23(typescript@5.9.3) + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@24.10.0)(jsdom@25.0.1)(terser@5.44.1))': dependencies: '@ampproject/remapping': 2.3.0 @@ -11866,6 +12281,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.3)(terser@5.44.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.3)(terser@5.44.1) + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.10.0)(terser@5.44.1))': dependencies: '@vitest/spy': 2.1.9 @@ -14732,6 +15155,8 @@ snapshots: filelist: 1.0.4 picocolors: 1.1.1 + javascript-natural-sort@0.7.1: {} + javascript-time-ago@2.5.12: dependencies: relative-time-format: 1.1.11 @@ -15332,6 +15757,8 @@ snapshots: dependencies: p-locate: 6.0.0 + lodash-es@4.17.22: {} + lodash.camelcase@4.3.0: {} lodash.debounce@4.0.8: {} @@ -17911,6 +18338,24 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 + vite-node@2.1.9(@types/node@22.19.3)(terser@5.44.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@5.5.0) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.19.3)(terser@5.44.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@2.1.9(@types/node@24.10.0)(terser@5.44.1): dependencies: cac: 6.7.14 @@ -17940,6 +18385,16 @@ snapshots: transitivePeerDependencies: - supports-color + vite@5.4.21(@types/node@22.19.3)(terser@5.44.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.5 + optionalDependencies: + '@types/node': 22.19.3 + fsevents: 2.3.3 + terser: 5.44.1 + vite@5.4.21(@types/node@24.10.0)(terser@5.44.1): dependencies: esbuild: 0.21.5 @@ -17950,6 +18405,22 @@ snapshots: fsevents: 2.3.3 terser: 5.44.1 + vite@7.2.1(@types/node@22.19.3)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.3 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.44.1 + tsx: 4.19.3 + yaml: 2.8.1 + vite@7.2.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(tsx@4.19.3)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -17966,12 +18437,54 @@ snapshots: tsx: 4.19.3 yaml: 2.8.1 + vitest-mock-extended@2.0.2(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1)): + dependencies: + ts-essentials: 10.1.1(typescript@5.9.3) + typescript: 5.9.3 + vitest: 2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1) + vitest-mock-extended@2.0.2(typescript@5.9.3)(vitest@2.1.9(@types/node@24.10.0)(jsdom@25.0.1)(terser@5.44.1)): dependencies: ts-essentials: 10.1.1(typescript@5.9.3) typescript: 5.9.3 vitest: 2.1.9(@types/node@24.10.0)(jsdom@25.0.1)(terser@5.44.1) + vitest@2.1.9(@types/node@22.19.3)(jsdom@25.0.1)(terser@5.44.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.3)(terser@5.44.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3(supports-color@5.5.0) + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.19.3)(terser@5.44.1) + vite-node: 2.1.9(@types/node@22.19.3)(terser@5.44.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.3 + jsdom: 25.0.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@2.1.9(@types/node@24.10.0)(jsdom@25.0.1)(terser@5.44.1): dependencies: '@vitest/expect': 2.1.9 From 69149039f8825556309d54693740823420f05c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Wed, 3 Dec 2025 16:15:38 +0100 Subject: [PATCH 03/33] chore: start integration old-server into nestjs --- apps/server-nestjs/src/app.controller.ts | 12 -- apps/server-nestjs/src/app.module.ts | 11 +- apps/server-nestjs/src/app.service.ts | 4 - .../src/cpin-module/cpin/cpin.service.ts | 12 +- .../src/cpin-module/old-server/src/connect.ts | 89 +++++---- .../cpin-module/old-server/src/prepare-app.ts | 171 +++++++++--------- apps/server-nestjs/src/main.ts | 2 +- 7 files changed, 144 insertions(+), 157 deletions(-) delete mode 100644 apps/server-nestjs/src/app.controller.ts delete mode 100644 apps/server-nestjs/src/app.service.ts diff --git a/apps/server-nestjs/src/app.controller.ts b/apps/server-nestjs/src/app.controller.ts deleted file mode 100644 index cce879ee6..000000000 --- a/apps/server-nestjs/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/apps/server-nestjs/src/app.module.ts b/apps/server-nestjs/src/app.module.ts index 465945f4e..75567a420 100644 --- a/apps/server-nestjs/src/app.module.ts +++ b/apps/server-nestjs/src/app.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; + import { CpinModule } from './cpin-module/cpin.module'; +// This module only exists to import other module. +// « One module to rule them all, and in NestJs bind them » @Module({ - imports: [CpinModule], - controllers: [AppController], - providers: [AppService], + imports: [CpinModule], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/apps/server-nestjs/src/app.service.ts b/apps/server-nestjs/src/app.service.ts deleted file mode 100644 index 7263d33a2..000000000 --- a/apps/server-nestjs/src/app.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService {} diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts index f8e75e7df..0e3aa4475 100644 --- a/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts +++ b/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts @@ -6,7 +6,7 @@ import fastifySwagger from '@fastify/swagger'; import fastifySwaggerUi from '@fastify/swagger-ui'; import { Injectable } from '@nestjs/common'; import { logger } from '@old-server/app'; -import { closeConnections } from '@old-server/connect'; +import { ConnectionService } from '@old-server/connect'; import { getPreparedApp } from '@old-server/prepare-app'; import { apiRouter } from '@old-server/resources/index.js'; import { @@ -34,7 +34,11 @@ import keycloak from 'fastify-keycloak-adapter'; @Injectable() export class CpinService { + constructor(private readonly connectionService: ConnectionService) {} + app: any; + serverInstance: ReturnType = initServer(); + logger: CustomLogger; handleExit() { process.on('exit', this.logExitCode); @@ -58,7 +62,7 @@ export class CpinService { } await this.app.close(); logger.info('Closing connections...'); - await closeConnections(); + await this.connectionService.closeConnections(); logger.info('Exiting...'); process.exit(error instanceof Error ? 1 : 0); } @@ -78,8 +82,6 @@ export class CpinService { } async createApp() { - const serverInstance: ReturnType = initServer(); - const openApiDocument = generateOpenApi( await getContract(), swaggerConf, @@ -138,6 +140,6 @@ export class CpinService { await app.ready(); - const logger = app.log as CustomLogger; + this.logger = app.log as CustomLogger; } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts index ae15b49d8..309f8fa97 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts @@ -1,52 +1,51 @@ -import { setTimeout } from 'node:timers/promises' -import prisma from './prisma.js' -import { logger } from './app.js' -import { - dbUrl, - isCI, - isDev, - isTest, -} from './utils/env.js' +import { Injectable } from '@nestjs/common'; +import { setTimeout } from 'node:timers/promises'; -const DELAY_BEFORE_RETRY = isTest || isCI ? 1000 : 10000 -let closingConnections = false +import { logger } from './app.js'; +import prisma from './prisma.js'; +import { dbUrl, isCI, isDev, isTest } from './utils/env.js'; -export async function getConnection(triesLeft = 5): Promise { - if (closingConnections || triesLeft <= 0) { - throw new Error('Unable to connect to Postgres server') - } - triesLeft-- +@Injectable() +export class ConnectionService { + closingConnections = false; - try { - if (isDev || isTest || isCI) { - logger.info(`Trying to connect to Postgres with: ${dbUrl}`) - } - await prisma.$connect() + async getConnection(triesLeft = 5): Promise { + if (this.closingConnections || triesLeft <= 0) { + throw new Error('Unable to connect to Postgres server'); + } + triesLeft--; - logger.info('Connected to Postgres!') - } catch (error) { - if (triesLeft > 0) { - logger.error(error) - logger.info(`Could not connect to Postgres: ${error.message}`) - logger.info(`Retrying (${triesLeft} tries left)`) - await setTimeout(DELAY_BEFORE_RETRY) - return getConnection(triesLeft) - } + try { + if (isDev || isTest || isCI) { + logger.info(`Trying to connect to Postgres with: ${dbUrl}`); + } + await prisma.$connect(); - logger.info(`Could not connect to Postgres: ${error.message}`) - logger.info('Out of retries') - error.message = `Out of retries, last error: ${error.message}` - throw error - } -} + logger.info('Connected to Postgres!'); + } catch (error) { + if (triesLeft > 0) { + logger.error(error); + logger.info(`Could not connect to Postgres: ${error.message}`); + logger.info(`Retrying (${triesLeft} tries left)`); + await setTimeout(isTest || isCI ? 1000 : 10000); + return this.getConnection(triesLeft); + } -export async function closeConnections() { - closingConnections = true - try { - await prisma.$disconnect() - } catch (error) { - logger.error(error) - } finally { - closingConnections = false - } + logger.info(`Could not connect to Postgres: ${error.message}`); + logger.info('Out of retries'); + error.message = `Out of retries, last error: ${error.message}`; + throw error; + } + } + + async closeConnections() { + this.closingConnections = true; + try { + await prisma.$disconnect(); + } catch (error) { + logger.error(error); + } finally { + this.closingConnections = false; + } + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts index 142f9b33e..f17f5648d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts @@ -1,106 +1,107 @@ -import { rm } from 'node:fs/promises' -import { dirname, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' -import { isCI, isDev, isDevSetup, isInt, isProd, isTest, port } from './utils/env.js' -import app, { logger } from './app.js' -import { getConnection } from './connect.js' -import { initDb } from './init/db/index.js' -import { initPm } from './plugins.js' +import { rm } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { ProxyAgent, setGlobalDispatcher } from 'undici'; + +import app, { logger } from './app'; +import { getConnection } from './connect'; +import { initDb } from './init/db/index'; +import { initPm } from './plugins'; +import { isCI, isDev, isDevSetup, isInt, isProd, isTest } from './utils/env'; // Workaround because fetch isn't using http_proxy variables // See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 if (process.env.HTTP_PROXY) { - const Undici = await import('undici') - const ProxyAgent = Undici.ProxyAgent - const setGlobalDispatcher = Undici.setGlobalDispatcher - setGlobalDispatcher( - new ProxyAgent(process.env.HTTP_PROXY), - ) + setGlobalDispatcher(new ProxyAgent(process.env.HTTP_PROXY)); } async function initializeDB(path: string) { - logger.info('Starting init DB...') - const { data } = await import(path) - await initDb(data) - logger.info('initDb invoked successfully') + logger.info('Starting init DB...'); + const { data } = await import(path); + await initDb(data); + logger.info('initDb invoked successfully'); } -export async function startServer(defaultPort: number = (port ? +port : 8080)) { - try { - await getConnection() - } catch (error) { - if (!(error instanceof Error)) return - logger.error(error.message) - throw error - } +export async function startServer() { + try { + await getConnection(); + } catch (error) { + if (!(error instanceof Error)) return; + logger.error(error.message); + throw error; + } - initPm() + initPm(); - logger.info('Reading init database file') + logger.info('Reading init database file'); - try { - const dataPath = (isProd || isInt) - ? './init/db/imports/data.js' - : '@cpn-console/test-utils/src/imports/data.ts' - await initializeDB(dataPath) - if (isProd && !isDevSetup) { - logger.info('Cleaning up imported data file...') - const __filename = fileURLToPath(import.meta.url) - const __dirname = dirname(__filename) - await rm(resolve(__dirname, dataPath)) - logger.info(`Successfully deleted '${dataPath}'`) - } - } catch (error) { - if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { - logger.info('No initDb file, skipping') - } else { - logger.warn(error.message) - throw error - } - } + // try { + // const dataPath = + // isProd || isInt + // ? './init/db/imports/data.js' + // : '@cpn-console/test-utils/src/imports/data.ts'; + // await initializeDB(dataPath); + // if (isProd && !isDevSetup) { + // logger.info('Cleaning up imported data file...'); + // const __filename = fileURLToPath(import.meta.url); + // const __dirname = dirname(__filename); + // await rm(resolve(__dirname, dataPath)); + // logger.info(`Successfully deleted '${dataPath}'`); + // } + // } catch (error) { + // if ( + // error.code === 'ERR_MODULE_NOT_FOUND' || + // error.message.includes('Failed to load') || + // error.message.includes('Cannot find module') + // ) { + // logger.info('No initDb file, skipping'); + // } else { + // logger.warn(error.message); + // throw error; + // } + // } - try { - await app.listen({ host: '0.0.0.0', port: defaultPort ?? 8080 }) - } catch (error) { - logger.error(error) - process.exit(1) - } - logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) + logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }); } export async function getPreparedApp() { - try { - await getConnection() - } catch (error) { - logger.error(error.message) - throw error - } + try { + await getConnection(); + } catch (error) { + logger.error(error.message); + throw error; + } - initPm() + initPm(); - logger.info('Reading init database file') + logger.info('Reading init database file'); - try { - const dataPath = (isProd || isInt) - ? './init/db/imports/data.js' - : '@cpn-console/test-utils/src/imports/data.ts' - await initializeDB(dataPath) - if (isProd && !isDevSetup) { - logger.info('Cleaning up imported data file...') - const __filename = fileURLToPath(import.meta.url) - const __dirname = dirname(__filename) - await rm(resolve(__dirname, dataPath)) - logger.info(`Successfully deleted '${dataPath}'`) - } - } catch (error) { - if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { - logger.info('No initDb file, skipping') - } else { - logger.warn(error.message) - throw error - } - } + // try { + // const dataPath = + // isProd || isInt + // ? './init/db/imports/data.js' + // : '@cpn-console/test-utils/src/imports/data.ts'; + // await initializeDB(dataPath); + // if (isProd && !isDevSetup) { + // logger.info('Cleaning up imported data file...'); + // const __filename = fileURLToPath(import.meta.url); + // const __dirname = dirname(__filename); + // await rm(resolve(__dirname, dataPath)); + // logger.info(`Successfully deleted '${dataPath}'`); + // } + // } catch (error) { + // if ( + // error.code === 'ERR_MODULE_NOT_FOUND' || + // error.message.includes('Failed to load') || + // error.message.includes('Cannot find module') + // ) { + // logger.info('No initDb file, skipping'); + // } else { + // logger.warn(error.message); + // throw error; + // } + // } - logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) - return app + logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }); + return app; } diff --git a/apps/server-nestjs/src/main.ts b/apps/server-nestjs/src/main.ts index f76bc8d97..e02a6e353 100644 --- a/apps/server-nestjs/src/main.ts +++ b/apps/server-nestjs/src/main.ts @@ -3,6 +3,6 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(process.env.PORT ?? 3000); + await app.listen(process.env.PORT ?? 8080); } bootstrap(); From f820fc602309cbcd9dbbc8759f2a257358571489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Wed, 3 Dec 2025 16:18:20 +0100 Subject: [PATCH 04/33] chore: update old-server import aliases --- .../old-server/src/connect.spec.ts | 2 +- .../old-server/src/init/db/dump.ts | 2 +- .../old-server/src/init/db/index.ts | 4 +-- .../src/resources/admin-role/business.ts | 8 +++--- .../src/resources/admin-role/queries.ts | 2 +- .../src/resources/admin-role/router.ts | 6 ++-- .../src/resources/admin-token/business.ts | 2 +- .../src/resources/admin-token/router.ts | 4 +-- .../src/resources/cluster/business.ts | 14 +++++----- .../src/resources/cluster/queries.ts | 2 +- .../src/resources/cluster/router.ts | 8 +++--- .../src/resources/environment/business.ts | 10 +++---- .../src/resources/environment/queries.ts | 2 +- .../src/resources/environment/router.ts | 6 ++-- .../old-server/src/resources/index.ts | 2 +- .../old-server/src/resources/log/business.ts | 2 +- .../old-server/src/resources/log/queries.ts | 2 +- .../old-server/src/resources/log/router.ts | 8 +++--- .../src/resources/project-member/business.ts | 8 +++--- .../src/resources/project-member/queries.ts | 2 +- .../src/resources/project-member/router.ts | 6 ++-- .../src/resources/project-role/business.ts | 6 ++-- .../src/resources/project-role/queries.ts | 2 +- .../src/resources/project-role/router.ts | 6 ++-- .../src/resources/project-service/business.ts | 2 +- .../src/resources/project-service/queries.ts | 2 +- .../src/resources/project-service/router.ts | 6 ++-- .../src/resources/project/business.ts | 16 +++++------ .../src/resources/project/queries.ts | 6 ++-- .../src/resources/project/router.ts | 6 ++-- .../old-server/src/resources/queries-index.ts | 28 +++++++++---------- .../src/resources/repository/business.ts | 6 ++-- .../src/resources/repository/queries.ts | 2 +- .../src/resources/repository/router.ts | 8 +++--- .../src/resources/service-chain/business.ts | 2 +- .../src/resources/service-chain/router.ts | 8 +++--- .../src/resources/service-monitor/router.ts | 6 ++-- .../src/resources/stage/business.ts | 6 ++-- .../old-server/src/resources/stage/queries.ts | 2 +- .../old-server/src/resources/stage/router.ts | 6 ++-- .../src/resources/system/config/business.ts | 2 +- .../src/resources/system/config/queries.ts | 2 +- .../src/resources/system/config/router.ts | 6 ++-- .../old-server/src/resources/system/router.ts | 4 +-- .../src/resources/system/settings/queries.ts | 2 +- .../src/resources/system/settings/router.ts | 6 ++-- .../old-server/src/resources/user/business.ts | 8 +++--- .../old-server/src/resources/user/queries.ts | 2 +- .../old-server/src/resources/user/router.ts | 8 +++--- .../src/resources/user/tokens/business.ts | 2 +- .../src/resources/user/tokens/router.ts | 8 +++--- .../old-server/src/resources/zone/business.ts | 6 ++-- .../old-server/src/resources/zone/queries.ts | 2 +- .../old-server/src/resources/zone/router.ts | 6 ++-- .../old-server/src/utils/controller.ts | 6 ++-- .../old-server/src/utils/hook-wrapper.ts | 6 ++-- .../old-server/src/utils/logger.ts | 2 +- 57 files changed, 153 insertions(+), 153 deletions(-) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts index 2c1a40cee..d74608f8e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts @@ -5,7 +5,7 @@ import app, { logger } from './app.js' import { getConnection } from './connect.js' vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) -vi.mock('@/resources/queries-index.js') +vi.mock('@old-server/resources/queries-index.js') vi.mock('./models/log.js', () => getModel('getLogModel')) vi.mock('./models/repository.js', () => getModel('getRepositoryModel')) vi.mock('./models/permission.js', () => getModel('getPermissionModel')) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts index 42e6c75cb..c9e29b8fa 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts @@ -10,7 +10,7 @@ import { writeFileSync } from 'node:fs' import { Prisma } from '@prisma/client' import { associations, manyToManyRelation, modelKeys, models, resourceListToDict } from './utils.js' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' const Models = resourceListToDict(Prisma.dmmf.datamodel.models) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts index a2788130e..b834f824d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts @@ -1,6 +1,6 @@ import { modelKeys } from './utils.js' -import { logger } from '@/app.js' -import prisma from '@/prisma.js' +import { logger } from '@old-server/app.js' +import prisma from '@old-server/prisma.js' type ExtractKeysWithFields = { [K in keyof T]: T[K] extends { fields: any } ? K : never diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts index a43cebf22..29bfb9067 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts @@ -2,10 +2,10 @@ import type { Project, ProjectRole } from '@prisma/client' import type { AdminRole, adminRoleContract } from '@cpn-console/shared' import { listAdminRoles, -} from '@/resources/queries-index.js' -import type { ErrorResType } from '@/utils/errors.js' -import { BadRequest400 } from '@/utils/errors.js' -import prisma from '@/prisma.js' +} from '@old-server/resources/queries-index.js' +import type { ErrorResType } from '@old-server/utils/errors.js' +import { BadRequest400 } from '@old-server/utils/errors.js' +import prisma from '@old-server/prisma.js' export async function listRoles() { return listAdminRoles() diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts index f4893b317..83fd52444 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts @@ -2,7 +2,7 @@ import type { AdminRole, Prisma, } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' export const listAdminRoles = () => prisma.adminRole.findMany({ orderBy: { position: 'asc' } }) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts index 18b1fc226..d69ea4a43 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts @@ -6,9 +6,9 @@ import { listRoles, patchRoles, } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' export function adminRoleRouter() { return serverInstance.router(adminRoleContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts index c5af30a43..8acbb11e6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts @@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto' import { type adminTokenContract, generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' import type { $Enums, AdminToken, Prisma } from '@prisma/client' import prisma from '../../prisma.js' -import { BadRequest400 } from '@/utils/errors.js' +import { BadRequest400 } from '@old-server/utils/errors.js' export async function listTokens(query: typeof adminTokenContract.listAdminTokens.query._type) { const where = { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts index c5a630e8d..bd3afb29e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts @@ -1,8 +1,8 @@ import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared' import { serverInstance } from '../../app.js' import { createToken, deleteToken, listTokens } from './business.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@/utils/errors.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' export function adminTokenRouter() { return serverInstance.router(adminTokenContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts index cff0b1882..04a0e6858 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts @@ -17,13 +17,13 @@ import { removeClusterFromProject, removeClusterFromStage, updateCluster as updateClusterQuery, -} from '@/resources/queries-index.js' -import { linkClusterToStages } from '@/resources/stage/business.js' -import { validateSchema } from '@/utils/business.js' -import { hook } from '@/utils/hook-wrapper.js' -import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '@/utils/errors.js' -import prisma from '@/prisma.js' -import type { Resources } from '@/types/index.js' +} from '@old-server/resources/queries-index.js' +import { linkClusterToStages } from '@old-server/resources/stage/business.js' +import { validateSchema } from '@old-server/utils/business.js' +import { hook } from '@old-server/utils/hook-wrapper.js' +import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '@old-server/utils/errors.js' +import prisma from '@old-server/prisma.js' +import type { Resources } from '@old-server/types/index.js' export async function listClusters(userId?: User['id']) { const where: Prisma.ClusterWhereInput = userId diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts index f11ccb33e..b49190161 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts @@ -1,5 +1,5 @@ import type { Cluster, Environment, Kubeconfig, Prisma, Project, Stage } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' export async function getClustersAssociatedWithProject(projectId: Project['id']) { const [ diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts index ffa5de8c4..d99c7aa1f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts @@ -9,10 +9,10 @@ import { listClusters, updateCluster, } from './business.js' -import '@/types/index.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@/utils/errors.js' +import '@old-server/types/index.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors.js' export function clusterRouter() { return serverInstance.router(clusterContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts index b7f5b5c34..17ae7bd7b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts @@ -5,11 +5,11 @@ import { getEnvironmentsByProjectId, initializeEnvironment, updateEnvironment as updateEnvironmentQuery, -} from '@/resources/queries-index.js' -import type { Resources, UserDetails } from '@/types/index.js' -import { hook } from '@/utils/hook-wrapper.js' -import prisma from '@/prisma.js' -import { Result } from '@/utils/business.js' +} from '@old-server/resources/queries-index.js' +import type { Resources, UserDetails } from '@old-server/types/index.js' +import { hook } from '@old-server/utils/hook-wrapper.js' +import prisma from '@old-server/prisma.js' +import { Result } from '@old-server/utils/business.js' export function getProjectEnvironments(projectId: Project['id']) { return getEnvironmentsByProjectId(projectId) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts index 620e693cb..019923c37 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts @@ -1,5 +1,5 @@ import type { Environment, Prisma, Project } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' // SELECT export function getEnvironmentByIdOrThrow(id: Environment['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts index f1cf21b8a..8a5c8b495 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts @@ -1,8 +1,8 @@ import { ProjectAuthorized, environmentContract } from '@cpn-console/shared' import { checkEnvironmentCreate, checkEnvironmentUpdate, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401 } from '@old-server/utils/errors.js' export function environmentRouter() { return serverInstance.router(environmentContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts index 5c0c48f4d..930f757ea 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts @@ -1,5 +1,5 @@ import type { FastifyInstance } from 'fastify' -import { serverInstance } from '@/app.js' +import { serverInstance } from '@old-server/app.js' import { adminRoleRouter } from './admin-role/router.js' import { adminTokenRouter } from './admin-token/router.js' diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts index 9a0182e7e..37d9f1fb3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts @@ -1,6 +1,6 @@ import type { logContract } from '@cpn-console/shared' import { CleanLogSchema } from '@cpn-console/shared' -import { getAllLogs } from '@/resources/queries-index.js' +import { getAllLogs } from '@old-server/resources/queries-index.js' export async function getLogs({ offset, limit, projectId, clean }: typeof logContract.getLogs.query._type) { const [total, logs] = await getAllLogs({ skip: offset, take: limit, where: { projectId } }) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts index 3851a8f13..167b01369 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts @@ -1,6 +1,6 @@ import type { Log, Prisma, Project, User } from '@prisma/client' import { exclude } from '@cpn-console/shared' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' // SELECT export function getAllLogsForUser(user: User, offset = 0) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts index e3e3247cf..13affd6e7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts @@ -1,10 +1,10 @@ import type { CleanLog, Log, XOR } from '@cpn-console/shared' import { AdminAuthorized, logContract } from '@cpn-console/shared' import { getLogs } from './business.js' -import { serverInstance } from '@/app.js' -import type { UserProfile, UserProjectProfile } from '@/utils/controller.js' -import { authUser } from '@/utils/controller.js' -import { Forbidden403 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import type { UserProfile, UserProjectProfile } from '@old-server/utils/controller.js' +import { authUser } from '@old-server/utils/controller.js' +import { Forbidden403 } from '@old-server/utils/errors.js' export function logRouter() { return serverInstance.router(logContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts index 392b7dd15..81e0155a0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts @@ -7,10 +7,10 @@ import { deleteMember, listMembers as listMembersQuery, upsertMember, -} from '@/resources/queries-index.js' -import prisma from '@/prisma.js' -import { BadRequest400, NotFound404 } from '@/utils/errors.js' -import { hook } from '@/utils/hook-wrapper.js' +} from '@old-server/resources/queries-index.js' +import prisma from '@old-server/prisma.js' +import { BadRequest400, NotFound404 } from '@old-server/utils/errors.js' +import { hook } from '@old-server/utils/hook-wrapper.js' export const listMembers = async (projectId: Project['id']) => listMembersQuery(projectId) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts index a4ceb00df..0161cc776 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts @@ -4,7 +4,7 @@ import type { Project, } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' export const listMembers = (projectId: Project['id']) => prisma.projectMembers.findMany({ where: { projectId }, include: { user: true } }) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts index 6b0e3f80e..905e99f8c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts @@ -5,9 +5,9 @@ import { patchMembers, removeMember, } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors.js' export function projectMemberRouter() { return serverInstance.router(projectMemberContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts index 3d20dc13c..8631d12a2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts @@ -5,9 +5,9 @@ import { listMembers, listRoles as listRolesQuery, updateRole, -} from '@/resources/queries-index.js' -import { BadRequest400 } from '@/utils/errors.js' -import prisma from '@/prisma.js' +} from '@old-server/resources/queries-index.js' +import { BadRequest400 } from '@old-server/utils/errors.js' +import prisma from '@old-server/prisma.js' export async function listRoles(projectId: Project['id']) { return listRolesQuery(projectId) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts index d915849eb..25690dff2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts @@ -5,7 +5,7 @@ import type { ProjectRole, } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' export const listRoles = (projectId: Project['id']) => prisma.projectRole.findMany({ where: { projectId }, orderBy: { position: 'asc' } }) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts index 2d55a2b84..d11db92bf 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts @@ -6,9 +6,9 @@ import { listRoles, patchRoles, } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403, NotFound404 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403, NotFound404 } from '@old-server/utils/errors.js' export function projectRoleRouter() { return serverInstance.router(projectRoleContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts index e11890d8e..0341b768f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts @@ -12,7 +12,7 @@ import { getProjectStore, getPublicClusters, saveProjectStore, -} from '@/resources/queries-index.js' +} from '@old-server/resources/queries-index.js' export type ConfigRecords = { key: string diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts index cf353614b..19e7ddea7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts @@ -1,6 +1,6 @@ import type { Project } from '@prisma/client' import type { ConfigRecords } from './business.js' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' // CONFIG export function getProjectStore(projectId: Project['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts index e79ccb4ac..c0713e5ea 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts @@ -1,8 +1,8 @@ import { AdminAuthorized, ProjectAuthorized, projectServiceContract } from '@cpn-console/shared' import { getProjectServices, updateProjectServices } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { Forbidden403, NotFound404 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { Forbidden403, NotFound404 } from '@old-server/utils/errors.js' export function projectServiceRouter() { return serverInstance.router(projectServiceContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts index e81ac8d3e..713b99003 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts @@ -14,14 +14,14 @@ import { listProjects as listProjectsQuery, lockProject, updateProject as updateProjectQuery, -} from '@/resources/queries-index.js' -import type { ErrorResType } from '@/utils/errors.js' -import { BadRequest400, Forbidden403, Unprocessable422 } from '@/utils/errors.js' -import { whereBuilder } from '@/utils/controller.js' -import { hook } from '@/utils/hook-wrapper.js' -import type { UserDetails } from '@/types/index.js' -import prisma from '@/prisma.js' -import { parallelBulkLimit } from '@/utils/env.js' +} from '@old-server/resources/queries-index.js' +import type { ErrorResType } from '@old-server/utils/errors.js' +import { BadRequest400, Forbidden403, Unprocessable422 } from '@old-server/utils/errors.js' +import { whereBuilder } from '@old-server/utils/controller.js' +import { hook } from '@old-server/utils/hook-wrapper.js' +import type { UserDetails } from '@old-server/types/index.js' +import prisma from '@old-server/prisma.js' +import { parallelBulkLimit } from '@old-server/utils/env.js' export function generateSlug(prefix: string, existingSlugs?: string[]) { if (!existingSlugs?.includes(prefix)) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts index 23544d1d2..ea4ae31f8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts @@ -7,9 +7,9 @@ import { ProjectStatus, } from '@prisma/client' import type { XOR, projectContract } from '@cpn-console/shared' -import prisma from '@/prisma.js' -import { appVersion } from '@/utils/env.js' -import { uuid } from '@/utils/queries-tools.js' +import prisma from '@old-server/prisma.js' +import { appVersion } from '@old-server/utils/env.js' +import { uuid } from '@old-server/utils/queries-tools.js' type ProjectUpdate = Partial> export function updateProject(id: Project['id'], data: ProjectUpdate) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts index d08d17f88..ecf13df98 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts @@ -11,9 +11,9 @@ import { replayHooks, updateProject, } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors.js' export function projectRouter() { return serverInstance.router(projectContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts index 4f0e11716..909657a6d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts @@ -1,14 +1,14 @@ -export * from '@/resources/admin-role/queries.js' -export * from '@/resources/cluster/queries.js' -export * from '@/resources/service-chain/queries.js' -export * from '@/resources/environment/queries.js' -export * from '@/resources/log/queries.js' -export * from '@/resources/project/queries.js' -export * from '@/resources/project-member/queries.js' -export * from '@/resources/project-role/queries.js' -export * from '@/resources/project-service/queries.js' -export * from '@/resources/repository/queries.js' -export * from '@/resources/user/queries.js' -export * from '@/resources/stage/queries.js' -export * from '@/resources/zone/queries.js' -export * from '@/resources/system/settings/queries.js' +export * from '@old-server/resources/admin-role/queries.js' +export * from '@old-server/resources/cluster/queries.js' +export * from '@old-server/resources/service-chain/queries.js' +export * from '@old-server/resources/environment/queries.js' +export * from '@old-server/resources/log/queries.js' +export * from '@old-server/resources/project/queries.js' +export * from '@old-server/resources/project-member/queries.js' +export * from '@old-server/resources/project-role/queries.js' +export * from '@old-server/resources/project-service/queries.js' +export * from '@old-server/resources/repository/queries.js' +export * from '@old-server/resources/user/queries.js' +export * from '@old-server/resources/stage/queries.js' +export * from '@old-server/resources/zone/queries.js' +export * from '@old-server/resources/system/settings/queries.js' diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts index 378990d3a..31d8ad813 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts @@ -1,8 +1,8 @@ import type { Project, Repository, User } from '@prisma/client' import type { CreateRepositoryBody, UpdateRepositoryBody } from '@cpn-console/shared' -import { addLogs, deleteRepository as deleteRepositoryQuery, getProjectInfosAndRepos, getProjectRepositories as getProjectRepositoriesQuery, initializeRepository, updateRepository as updateRepositoryQuery } from '@/resources/queries-index.js' -import { BadRequest400, Unprocessable422 } from '@/utils/errors.js' -import { hook } from '@/utils/hook-wrapper.js' +import { addLogs, deleteRepository as deleteRepositoryQuery, getProjectInfosAndRepos, getProjectRepositories as getProjectRepositoriesQuery, initializeRepository, updateRepository as updateRepositoryQuery } from '@old-server/resources/queries-index.js' +import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors.js' +import { hook } from '@old-server/utils/hook-wrapper.js' export async function getProjectRepositories(projectId: Project['id']) { return getProjectRepositoriesQuery(projectId) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts index 48992cd66..f4ee871fe 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts @@ -1,5 +1,5 @@ import type { Project, Repository } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' // SELECT export function getRepositoryById(id: Repository['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts index 42e0e2b4d..02b9c14db 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts @@ -6,11 +6,11 @@ import { syncRepository, updateRepository, } from './business.js' -import { serverInstance } from '@/app.js' +import { serverInstance } from '@old-server/app.js' -import { filterObjectByKeys } from '@/utils/queries-tools.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@/utils/errors.js' +import { filterObjectByKeys } from '@old-server/utils/queries-tools.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors.js' export function repositoryRouter() { return serverInstance.router(repositoryContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts index c245279d4..baabb00a0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts @@ -4,7 +4,7 @@ import { retryServiceChain as retryServiceChainQuery, validateServiceChain as validateServiceChainQuery, getServiceChainFlows as getServiceChainFlowsQuery, -} from '@/resources/queries-index.js' +} from '@old-server/resources/queries-index.js' export async function listServiceChains() { return listServiceChainsQuery() diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts index 0f53f3c16..adada395a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts @@ -7,10 +7,10 @@ import { validateServiceChain as validateServiceChainBusiness, getServiceChainFlows as getServiceChainFlowsBusiness, } from './business.js' -import '@/types/index.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { Forbidden403 } from '@/utils/errors.js' +import '@old-server/types/index.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { Forbidden403 } from '@old-server/utils/errors.js' export function serviceChainRouter() { return serverInstance.router(serviceChainContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts index a12fea66e..6e5282767 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts @@ -1,8 +1,8 @@ import { AdminAuthorized, serviceContract } from '@cpn-console/shared' import { checkServicesHealth, refreshServicesHealth } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { Forbidden403 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { Forbidden403 } from '@old-server/utils/errors.js' export function serviceMonitorRouter() { return serverInstance.router(serviceContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts index eb5110ec8..d80296d3b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts @@ -12,9 +12,9 @@ import { listStages as listStagesQuery, removeClusterFromStage, updateStageName, -} from '@/resources/queries-index.js' -import { BadRequest400, NotFound404 } from '@/utils/errors.js' -import prisma from '@/prisma.js' +} from '@old-server/resources/queries-index.js' +import { BadRequest400, NotFound404 } from '@old-server/utils/errors.js' +import prisma from '@old-server/prisma.js' export async function getStageAssociatedEnvironments(stageId: Stage['id']) { const environments = await getStageAssociatedEnvironmentById(stageId) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts index 98d526600..d5d4ea848 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts @@ -1,5 +1,5 @@ import type { Cluster, Stage } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' export function listStages() { return prisma.stage.findMany({ diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts index 8a2f5f9a5..710df2782 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts @@ -6,10 +6,10 @@ import { listStages, updateStage, } from './business.js' -import { serverInstance } from '@/app.js' +import { serverInstance } from '@old-server/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@/utils/errors.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' export function stageRouter() { return serverInstance.router(stageContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts index 9b89e9ef0..86263d861 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts @@ -6,7 +6,7 @@ import { getAdminPlugin, savePluginsConfig, } from './queries.js' -import { BadRequest400 } from '@/utils/errors.js' +import { BadRequest400 } from '@old-server/utils/errors.js' export type ConfigRecords = { key: string diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts index 69808e506..5d56aeb47 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts @@ -1,5 +1,5 @@ import type { ConfigRecords } from './business.js' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' // CONFIG export const getAdminPlugin = prisma.adminPlugin.findMany diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts index 534254e6e..2e4bffb11 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts @@ -1,8 +1,8 @@ import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared' import { getPluginsConfig, updatePluginConfig } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' export function pluginConfigRouter() { return serverInstance.router(systemPluginContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts index 997688496..3ec017a3c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts @@ -1,6 +1,6 @@ import { systemContract } from '@cpn-console/shared' -import { serverInstance } from '@/app.js' -import { appVersion } from '@/utils/env.js' +import { serverInstance } from '@old-server/app.js' +import { appVersion } from '@old-server/utils/env.js' export function systemRouter() { return serverInstance.router(systemContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts index c64cb3b74..15b352288 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts @@ -1,5 +1,5 @@ import type { Prisma, SystemSetting } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' export function upsertSystemSetting(newSystemSetting: SystemSetting) { return prisma.systemSetting.upsert({ diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts index baa0765b6..b2c1114fc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts @@ -1,8 +1,8 @@ import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared' import { getSystemSettings, upsertSystemSetting } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { Forbidden403 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { Forbidden403 } from '@old-server/utils/errors.js' export function systemSettingsRouter() { return serverInstance.router(systemSettingsContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts index ddaef0a01..052d4f92f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts @@ -1,10 +1,10 @@ import { createHash } from 'node:crypto' import type { AdminRole, AdminToken, PersonalAccessToken, Prisma, User } from '@prisma/client' import type { XOR, userContract } from '@cpn-console/shared' -import { getMatchingUsers as getMatchingUsersQuery, getUsers as getUsersQuery } from '@/resources/queries-index.js' -import prisma from '@/prisma.js' -import type { UserDetails } from '@/types/index.js' -import { BadRequest400 } from '@/utils/errors.js' +import { getMatchingUsers as getMatchingUsersQuery, getUsers as getUsersQuery } from '@old-server/resources/queries-index.js' +import prisma from '@old-server/prisma.js' +import type { UserDetails } from '@old-server/types/index.js' +import { BadRequest400 } from '@old-server/utils/errors.js' export async function getUsers(query: typeof userContract.getAllUsers.query._type, relationType: 'OR' | 'AND' = 'AND') { const whereInputs: Prisma.UserWhereInput[] = [] diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts index 1e33a8128..fe2559c79 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts @@ -1,5 +1,5 @@ import type { Prisma, User } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' type UserCreate = Omit diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts index c95d0620c..f5369dd1b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts @@ -5,10 +5,10 @@ import { logViaSession, patchUsers, } from './business.js' -import '@/types/index.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@/utils/errors.js' +import '@old-server/types/index.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors.js' export function userRouter() { return serverInstance.router(userContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts index 4e6da3e6e..8d491775d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts @@ -3,7 +3,7 @@ import type { personalAccessTokenContract } from '@cpn-console/shared' import { generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' import type { AdminToken, User } from '@prisma/client' import prisma from '../../../prisma.js' -import { BadRequest400 } from '@/utils/errors.js' +import { BadRequest400 } from '@old-server/utils/errors.js' export async function listTokens(userId: User['id']) { return prisma.personalAccessToken.findMany({ diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts index 4dfdc134d..5777f5cb5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts @@ -1,10 +1,10 @@ import { personalAccessTokenContract } from '@cpn-console/shared' -import '@/types/index.js' +import '@old-server/types/index.js' import { createToken, deleteToken, listTokens } from './business.js' -import { serverInstance } from '@/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@/utils/errors.js' +import { serverInstance } from '@old-server/app.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' export function personalAccessTokenRouter() { return serverInstance.router(personalAccessTokenContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts index c0cd25979..c5a0b7d29 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts @@ -1,9 +1,9 @@ import type { User, Zone } from '@cpn-console/shared' import { addLogs } from '../queries-index.js' import { linkZoneToClusters } from './queries.js' -import { BadRequest400, Unprocessable422 } from '@/utils/errors.js' -import prisma from '@/prisma.js' -import { hook } from '@/utils/hook-wrapper.js' +import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors.js' +import prisma from '@old-server/prisma.js' +import { hook } from '@old-server/utils/hook-wrapper.js' export const listZones = prisma.zone.findMany diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts index 1390bb153..255287000 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts @@ -1,5 +1,5 @@ import type { Cluster, Zone } from '@prisma/client' -import prisma from '@/prisma.js' +import prisma from '@old-server/prisma.js' export function getZoneByIdOrThrow(id: Zone['id']) { return prisma.zone.findUniqueOrThrow({ diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts index da9931329..4a583a2f5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts @@ -1,9 +1,9 @@ import { AdminAuthorized, zoneContract } from '@cpn-console/shared' import { createZone, deleteZone, listZones, updateZone } from './business.js' -import { serverInstance } from '@/app.js' +import { serverInstance } from '@old-server/app.js' -import { authUser } from '@/utils/controller.js' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@/utils/errors.js' +import { authUser } from '@old-server/utils/controller.js' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors.js' export function zoneRouter() { return serverInstance.router(zoneContract, { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts index c5b1c72f0..ba3eb8425 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts @@ -4,9 +4,9 @@ import { PROJECT_PERMS as PP, PROJECT_PERMS, projectIsLockedInfo, tokenHeaderNam import type { FastifyRequest } from 'fastify' import { Unauthorized401 } from './errors.js' import { uuid } from './queries-tools.js' -import type { UserDetails } from '@/types/index.js' -import prisma from '@/prisma.js' -import { logViaSession, logViaToken } from '@/resources/user/business.js' +import type { UserDetails } from '@old-server/types/index.js' +import prisma from '@old-server/prisma.js' +import { logViaSession, logViaToken } from '@old-server/resources/user/business.js' export type RequireOnlyOne = Pick> diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts index 4a248450e..06c3e4ab9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts @@ -4,9 +4,9 @@ import { hooks } from '@cpn-console/hooks' import type { AsyncReturnType } from '@cpn-console/shared' import { ProjectAuthorized, getPermsByUserRoles, resourceListToDict } from '@cpn-console/shared' import { genericProxy } from './proxy.js' -import { archiveProject, getAdminPlugin, getClusterByIdOrThrow, getClusterNamesByZoneId, getClustersAssociatedWithProject, getHookProjectInfos, getHookRepository, getProjectStore, getZoneByIdOrThrow, saveProjectStore, updateProjectClusterHistory, updateProjectCreated, updateProjectFailed, updateProjectWarning } from '@/resources/queries-index.js' -import type { ConfigRecords } from '@/resources/project-service/business.js' -import { dbToObj } from '@/resources/project-service/business.js' +import { archiveProject, getAdminPlugin, getClusterByIdOrThrow, getClusterNamesByZoneId, getClustersAssociatedWithProject, getHookProjectInfos, getHookRepository, getProjectStore, getZoneByIdOrThrow, saveProjectStore, updateProjectClusterHistory, updateProjectCreated, updateProjectFailed, updateProjectWarning } from '@old-server/resources/queries-index.js' +import type { ConfigRecords } from '@old-server/resources/project-service/business.js' +import { dbToObj } from '@old-server/resources/project-service/business.js' export type ReposCreds = Record export type ProjectInfos = AsyncReturnType diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts index 4857696ae..4483c8398 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts @@ -1,6 +1,6 @@ import type { FastifyBaseLogger, FastifyLogFn, PinoLoggerOptions } from 'fastify/types/logger.js' import type { XOR } from '@cpn-console/shared' -import { logger as customLogger } from '@/app.js' +import { logger as customLogger } from '@old-server/app.js' export const customLevels = { audit: 25, From cae9471d0f20a925d33668161558f67d4f3a219f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Wed, 3 Dec 2025 17:32:05 +0100 Subject: [PATCH 05/33] chore: apply prettier on old-server --- apps/server-nestjs/src/app.controller.spec.ts | 25 +- .../src/cpin-module/cpin.module.ts | 7 +- .../cpin-module/cpin/cpin.controller.spec.ts | 18 - .../src/cpin-module/cpin/cpin.controller.ts | 4 - .../src/cpin-module/cpin/cpin.service.spec.ts | 18 - .../src/cpin-module/cpin/cpin.service.ts | 145 --- .../cpin-module/old-server/prisma.config.ts | 14 +- .../old-server/src/__mocks__/prisma.ts | 18 +- .../src/__mocks__/utils/hook-wrapper.ts | 56 +- .../cpin-module/old-server/src/app.spec.ts | 37 +- .../src/cpin-module/old-server/src/app.ts | 133 +- .../old-server/src/connect.spec.ts | 132 +- .../old-server/src/init/db/dump.ts | 36 +- .../old-server/src/init/db/index.ts | 89 +- .../old-server/src/init/db/utils.spec.ts | 83 +- .../old-server/src/init/db/utils.ts | 146 ++- .../old-server/src/mocks/prisma.ts | 18 +- .../cpin-module/old-server/src/mocks/utils.ts | 26 +- .../src/cpin-module/old-server/src/plugins.ts | 90 +- .../old-server/src/prepare-app.spec.ts | 115 +- .../src/cpin-module/old-server/src/prisma.ts | 6 +- .../src/resources/admin-role/business.spec.ts | 398 +++--- .../src/resources/admin-role/business.ts | 173 +-- .../src/resources/admin-role/queries.ts | 54 +- .../src/resources/admin-role/router.spec.ts | 400 +++--- .../src/resources/admin-role/router.ts | 119 +- .../resources/admin-token/business.spec.ts | 139 ++- .../src/resources/admin-token/business.ts | 140 ++- .../src/resources/admin-token/router.spec.ts | 343 +++--- .../src/resources/admin-token/router.ts | 86 +- .../src/resources/cluster/business.spec.ts | 452 ++++--- .../src/resources/cluster/business.ts | 455 ++++--- .../src/resources/cluster/queries.ts | 537 ++++---- .../src/resources/cluster/router.spec.ts | 719 ++++++----- .../src/resources/cluster/router.ts | 261 ++-- .../resources/environment/business.spec.ts | 778 ++++++------ .../src/resources/environment/business.ts | 582 +++++---- .../src/resources/environment/queries.ts | 153 +-- .../src/resources/environment/router.spec.ts | 956 ++++++++------ .../src/resources/environment/router.ts | 229 ++-- .../old-server/src/resources/index.ts | 130 +- .../src/resources/log/business.spec.ts | 89 +- .../old-server/src/resources/log/business.ts | 26 +- .../old-server/src/resources/log/queries.ts | 98 +- .../src/resources/log/router.spec.ts | 199 +-- .../old-server/src/resources/log/router.ts | 59 +- .../src/resources/project-member/business.ts | 149 ++- .../src/resources/project-member/queries.ts | 55 +- .../resources/project-member/router.spec.ts | 746 ++++++----- .../src/resources/project-member/router.ts | 199 +-- .../resources/project-role/business.spec.ts | 429 ++++--- .../src/resources/project-role/business.ts | 142 ++- .../src/resources/project-role/queries.ts | 99 +- .../src/resources/project-role/router.spec.ts | 807 +++++++----- .../src/resources/project-role/router.ts | 213 ++-- .../src/resources/project-service/business.ts | 185 +-- .../src/resources/project-service/queries.ts | 96 +- .../resources/project-service/router.spec.ts | 412 ++++--- .../src/resources/project-service/router.ts | 91 +- .../src/resources/project/business.spec.ts | 845 +++++++------ .../src/resources/project/business.ts | 564 +++++---- .../src/resources/project/queries.ts | 545 ++++---- .../src/resources/project/router.spec.ts | 1097 ++++++++++------- .../src/resources/project/router.ts | 421 ++++--- .../old-server/src/resources/queries-index.ts | 28 +- .../src/resources/repository/business.ts | 236 ++-- .../src/resources/repository/queries.ts | 90 +- .../src/resources/repository/router.spec.ts | 1044 ++++++++++------ .../src/resources/repository/router.ts | 318 +++-- .../resources/service-chain/business.spec.ts | 329 ++--- .../src/resources/service-chain/business.ts | 22 +- .../src/resources/service-chain/queries.ts | 74 +- .../resources/service-chain/router.spec.ts | 582 +++++---- .../src/resources/service-chain/router.ts | 172 +-- .../src/resources/service-monitor/business.ts | 6 +- .../resources/service-monitor/router.spec.ts | 174 +-- .../src/resources/service-monitor/router.ts | 83 +- .../src/resources/stage/business.spec.ts | 270 ++-- .../src/resources/stage/business.ts | 164 +-- .../old-server/src/resources/stage/queries.ts | 157 +-- .../src/resources/stage/router.spec.ts | 471 ++++--- .../old-server/src/resources/stage/router.ts | 171 +-- .../resources/system/config/business.spec.ts | 43 +- .../src/resources/system/config/business.ts | 85 +- .../src/resources/system/config/queries.ts | 47 +- .../resources/system/config/router.spec.ts | 203 +-- .../src/resources/system/config/router.ts | 60 +- .../old-server/src/resources/system/index.ts | 2 +- .../src/resources/system/router.spec.ts | 46 +- .../old-server/src/resources/system/router.ts | 34 +- .../src/resources/system/settings/business.ts | 16 +- .../src/resources/system/settings/queries.ts | 29 +- .../resources/system/settings/router.spec.ts | 116 +- .../src/resources/system/settings/router.ts | 48 +- .../src/resources/user/business.spec.ts | 463 +++---- .../old-server/src/resources/user/business.ts | 448 ++++--- .../old-server/src/resources/user/queries.ts | 87 +- .../src/resources/user/router.spec.ts | 291 +++-- .../old-server/src/resources/user/router.ts | 104 +- .../src/resources/user/tokens/business.ts | 96 +- .../src/resources/user/tokens/router.ts | 91 +- .../src/resources/zone/business.spec.ts | 266 ++-- .../old-server/src/resources/zone/business.ts | 167 ++- .../old-server/src/resources/zone/queries.ts | 35 +- .../src/resources/zone/router.spec.ts | 366 +++--- .../old-server/src/resources/zone/router.ts | 114 +- .../cpin-module/old-server/src/server.spec.ts | 92 +- .../src/cpin-module/old-server/src/server.ts | 189 ++- .../old-server/src/utils/business.ts | 75 +- .../old-server/src/utils/controller.ts | 349 +++--- .../old-server/src/utils/date.spec.ts | 23 +- .../cpin-module/old-server/src/utils/date.ts | 4 +- .../cpin-module/old-server/src/utils/env.ts | 76 +- .../old-server/src/utils/errors.ts | 58 +- .../old-server/src/utils/fastify.ts | 91 +- .../old-server/src/utils/hook-wrapper.spec.ts | 456 +++---- .../old-server/src/utils/hook-wrapper.ts | 558 +++++---- .../src/utils/keycloak-utils.spec.ts | 77 +- .../old-server/src/utils/keycloak-utils.ts | 36 +- .../old-server/src/utils/keycloak.ts | 79 +- .../old-server/src/utils/logger.ts | 178 +-- .../cpin-module/old-server/src/utils/mocks.ts | 288 +++-- .../old-server/src/utils/plugins.ts | 15 +- .../old-server/src/utils/proxy.spec.ts | 306 ++--- .../cpin-module/old-server/src/utils/proxy.ts | 157 ++- .../src/utils/queries-tools.spec.ts | 84 +- .../old-server/src/utils/queries-tools.ts | 11 +- .../old-server/src/utils/random.spec.ts | 298 ++--- .../src/cpin-module/old-server/vite.config.ts | 25 +- .../src/cpin-module/old-server/vitest-init.ts | 22 +- .../cpin-module/old-server/vitest.config.ts | 64 +- apps/server-nestjs/src/main.ts | 5 +- apps/server-nestjs/test/app.e2e-spec.ts | 31 +- 133 files changed, 15458 insertions(+), 11723 deletions(-) delete mode 100644 apps/server-nestjs/src/cpin-module/cpin/cpin.controller.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/cpin/cpin.controller.ts delete mode 100644 apps/server-nestjs/src/cpin-module/cpin/cpin.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts diff --git a/apps/server-nestjs/src/app.controller.spec.ts b/apps/server-nestjs/src/app.controller.spec.ts index d22f3890a..4964aaf9d 100644 --- a/apps/server-nestjs/src/app.controller.spec.ts +++ b/apps/server-nestjs/src/app.controller.spec.ts @@ -1,22 +1,23 @@ import { Test, TestingModule } from '@nestjs/testing'; + import { AppController } from './app.controller'; import { AppService } from './app.service'; describe('AppController', () => { - let appController: AppController; + let appController: AppController; - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService], + }).compile(); - appController = app.get(AppController); - }); + appController = app.get(AppController); + }); - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); }); - }); }); diff --git a/apps/server-nestjs/src/cpin-module/cpin.module.ts b/apps/server-nestjs/src/cpin-module/cpin.module.ts index 935f688fe..4c39d4d2a 100644 --- a/apps/server-nestjs/src/cpin-module/cpin.module.ts +++ b/apps/server-nestjs/src/cpin-module/cpin.module.ts @@ -1,9 +1,8 @@ import { Module } from '@nestjs/common'; -import { CpinController } from './cpin/cpin.controller'; -import { CpinService } from './cpin/cpin.service'; +import { ServerService } from '@old-server/server'; @Module({ - controllers: [CpinController], - providers: [CpinService] + controllers: [], + providers: [ServerService], }) export class CpinModule {} diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.spec.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.spec.ts deleted file mode 100644 index a7d9aa118..000000000 --- a/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CpinController } from './cpin.controller'; - -describe('CpinController', () => { - let controller: CpinController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [CpinController], - }).compile(); - - controller = module.get(CpinController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.ts deleted file mode 100644 index 9ac472902..000000000 --- a/apps/server-nestjs/src/cpin-module/cpin/cpin.controller.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Controller } from '@nestjs/common'; - -@Controller('cpin') -export class CpinController {} diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.service.spec.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.service.spec.ts deleted file mode 100644 index 0386560c0..000000000 --- a/apps/server-nestjs/src/cpin-module/cpin/cpin.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CpinService } from './cpin.service'; - -describe('CpinService', () => { - let service: CpinService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [CpinService], - }).compile(); - - service = module.get(CpinService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts b/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts deleted file mode 100644 index 0e3aa4475..000000000 --- a/apps/server-nestjs/src/cpin-module/cpin/cpin.service.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { apiPrefix, getContract } from '@cpn-console/shared'; -import fastifyCookie from '@fastify/cookie'; -import helmet from '@fastify/helmet'; -import fastifySession from '@fastify/session'; -import fastifySwagger from '@fastify/swagger'; -import fastifySwaggerUi from '@fastify/swagger-ui'; -import { Injectable } from '@nestjs/common'; -import { logger } from '@old-server/app'; -import { ConnectionService } from '@old-server/connect'; -import { getPreparedApp } from '@old-server/prepare-app'; -import { apiRouter } from '@old-server/resources/index.js'; -import { - isCI, - isDev, - isDevSetup, - isInt, - isProd, - isTest, - port, -} from '@old-server/utils/env.js'; -import { - fastifyConf, - swaggerConf, - swaggerUiConf, -} from '@old-server/utils/fastify.js'; -import { keycloakConf, sessionConf } from '@old-server/utils/keycloak.js'; -import type { CustomLogger } from '@old-server/utils/logger.js'; -import { log } from '@old-server/utils/logger.js'; -import { initServer } from '@ts-rest/fastify'; -import { generateOpenApi } from '@ts-rest/open-api'; -import type { FastifyRequest } from 'fastify'; -import fastify from 'fastify'; -import keycloak from 'fastify-keycloak-adapter'; - -@Injectable() -export class CpinService { - constructor(private readonly connectionService: ConnectionService) {} - - app: any; - serverInstance: ReturnType = initServer(); - logger: CustomLogger; - - handleExit() { - process.on('exit', this.logExitCode); - process.on('SIGINT', this.exitGracefully); - process.on('SIGTERM', this.exitGracefully); - process.on('uncaughtException', this.exitGracefully); - process.on('unhandledRejection', this.logUnhandledRejection); - } - - logExitCode(code: number) { - logger.warn(`received signal: ${code}`); - } - - logUnhandledRejection(reason: unknown, promise: Promise) { - logger.error({ message: 'Unhandled Rejection', promise, reason }); - } - - async exitGracefully(error?: Error) { - if (error instanceof Error) { - logger.fatal(error); - } - await this.app.close(); - logger.info('Closing connections...'); - await this.connectionService.closeConnections(); - logger.info('Exiting...'); - process.exit(error instanceof Error ? 1 : 0); - } - - async getApp(): Promise { - const app = await getPreparedApp(); - - try { - await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }); - } catch (error) { - logger.error(error); - process.exit(1); - } - - logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }); - this.handleExit(); - } - - async createApp() { - const openApiDocument = generateOpenApi( - await getContract(), - swaggerConf, - { - setOperationId: true, - }, - ); - - const app = fastify(fastifyConf) - .register(helmet, () => ({ - contentSecurityPolicy: !(isInt || isDev || isTest), - })) - .register(fastifyCookie) - .register(fastifySession, sessionConf) - // @ts-ignore - .register(keycloak, keycloakConf) - .register(fastifySwagger, { - transformObject: () => openApiDocument, - }) - .register(fastifySwaggerUi, swaggerUiConf) - .register(apiRouter()) - .addHook('onRoute', (opts) => { - if (opts.path === `${apiPrefix}/healthz`) { - opts.logLevel = 'silent'; - } - }) - .setErrorHandler((error: Error, req: FastifyRequest, reply) => { - const statusCode = 500; - // @ts-ignore vérifier l'objet - const message = error.description || error.message; - reply.status(statusCode).send({ - status: statusCode, - error: message, - stack: error.stack, - }); - log('info', { reqId: req.id, error }); - }) - .addHook('onResponse', (req, res) => { - if (res.statusCode < 400) { - req.log.info({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } else if (res.statusCode < 500) { - req.log.warn({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } else { - req.log.error({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } - }); - - await app.ready(); - - this.logger = app.log as CustomLogger; - } -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts b/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts index 057121c97..1823401ec 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts @@ -1,9 +1,9 @@ -import path from 'node:path' -import { defineConfig } from 'prisma/config' +import path from 'node:path'; +import { defineConfig } from 'prisma/config'; export default defineConfig({ - schema: path.join('src', 'prisma', 'schema'), - migrations: { - path: path.join('src', 'prisma', 'migrations'), - }, -}) + schema: path.join('src', 'prisma', 'schema'), + migrations: { + path: path.join('src', 'prisma', 'migrations'), + }, +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts index 075578c96..9c88b20e7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts @@ -1,14 +1,14 @@ -import type { PrismaClient } from '@prisma/client' -import { beforeEach, vi } from 'vitest' -import { mockDeep, mockReset } from 'vitest-mock-extended' +import type { PrismaClient } from '@prisma/client'; +import { beforeEach, vi } from 'vitest'; +import { mockDeep, mockReset } from 'vitest-mock-extended'; -vi.mock('../prisma.js') +vi.mock('../prisma.js'); -const prisma = mockDeep() +const prisma = mockDeep(); beforeEach(() => { - // reset les mocks - mockReset(prisma) -}) + // reset les mocks + mockReset(prisma); +}); -export default prisma +export default prisma; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts index 34c7bee26..ac35e073d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts @@ -1,34 +1,34 @@ -import { beforeEach, vi } from 'vitest' -import { mockDeep, mockReset } from 'vitest-mock-extended' +import { beforeEach, vi } from 'vitest'; +import { mockDeep, mockReset } from 'vitest-mock-extended'; -vi.mock('../utils/hook-wrapper.ts') +vi.mock('../utils/hook-wrapper.ts'); export const hook = { - cluster: { - delete: vi.fn(), - upsert: vi.fn(), - }, - misc: { - checkServices: vi.fn(), - syncRepository: vi.fn(), - }, - project: { - upsert: vi.fn(), - delete: vi.fn(), - getSecrets: vi.fn(), - }, - user: { - retrieveUserByEmail: vi.fn(), - }, - zone: { - delete: vi.fn(), - upsert: vi.fn(), - }, -} as const + cluster: { + delete: vi.fn(), + upsert: vi.fn(), + }, + misc: { + checkServices: vi.fn(), + syncRepository: vi.fn(), + }, + project: { + upsert: vi.fn(), + delete: vi.fn(), + getSecrets: vi.fn(), + }, + user: { + retrieveUserByEmail: vi.fn(), + }, + zone: { + delete: vi.fn(), + upsert: vi.fn(), + }, +} as const; -const hookMock = mockDeep() +const hookMock = mockDeep(); beforeEach(() => { - // reset les mocks - mockReset(hookMock) -}) + // reset les mocks + mockReset(hookMock); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts index a09ea94b5..3e9b26571 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts @@ -1,21 +1,24 @@ -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -import { apiPrefix } from '@cpn-console/shared' -import app from './app.js' -import { getRandomRequestor, setRequestor } from './utils/mocks.js' +import { apiPrefix } from '@cpn-console/shared'; +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) +import app from './app.js'; +import { getRandomRequestor, setRequestor } from './utils/mocks.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('./utils/mocks.js')).mockSessionPlugin, +); describe('app', () => { - beforeEach(() => { - setRequestor(getRandomRequestor()) - }) - afterAll(async () => { - await app.close() - }) + beforeEach(() => { + setRequestor(getRandomRequestor()); + }); + afterAll(async () => { + await app.close(); + }); - it('should respond 404 on unknown route', async () => { - const response = await app.inject() - .get(`${apiPrefix}/miss`) - expect(response.statusCode).toBe(404) - }) -}) + it('should respond 404 on unknown route', async () => { + const response = await app.inject().get(`${apiPrefix}/miss`); + expect(response.statusCode).toBe(404); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts index c525cf161..2513e5717 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts @@ -1,59 +1,84 @@ -import type { FastifyRequest } from 'fastify' -import fastify from 'fastify' -import helmet from '@fastify/helmet' -import keycloak from 'fastify-keycloak-adapter' -import fastifySession from '@fastify/session' -import fastifyCookie from '@fastify/cookie' -import fastifySwagger from '@fastify/swagger' -import fastifySwaggerUi from '@fastify/swagger-ui' -import { initServer } from '@ts-rest/fastify' -import { generateOpenApi } from '@ts-rest/open-api' -import { apiPrefix, getContract } from '@cpn-console/shared' -import { isDev, isInt, isTest } from './utils/env.js' -import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify.js' -import { apiRouter } from './resources/index.js' -import { keycloakConf, sessionConf } from './utils/keycloak.js' -import type { CustomLogger } from './utils/logger.js' -import { log } from './utils/logger.js' +import { apiPrefix, getContract } from '@cpn-console/shared'; +import fastifyCookie from '@fastify/cookie'; +import helmet from '@fastify/helmet'; +import fastifySession from '@fastify/session'; +import fastifySwagger from '@fastify/swagger'; +import fastifySwaggerUi from '@fastify/swagger-ui'; +import { Injectable } from '@nestjs/common'; +import { initServer } from '@ts-rest/fastify'; +import { generateOpenApi } from '@ts-rest/open-api'; +import type { FastifyRequest } from 'fastify'; +import fastify from 'fastify'; +import keycloak from 'fastify-keycloak-adapter'; -export const serverInstance: ReturnType = initServer() +import { apiRouter } from './resources/index.js'; +import { isDev, isInt, isTest } from './utils/env.js'; +import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify.js'; +import { keycloakConf, sessionConf } from './utils/keycloak.js'; +import type { CustomLogger } from './utils/logger.js'; +import { log } from './utils/logger.js'; -const openApiDocument = generateOpenApi(await getContract(), swaggerConf, { setOperationId: true }) +@Injectable() +export class AppService { + serverInstance: ReturnType = initServer(); -const app = fastify(fastifyConf) - .register(helmet, () => ({ - contentSecurityPolicy: !(isInt || isDev || isTest), - })) - .register(fastifyCookie) - .register(fastifySession, sessionConf) - // @ts-ignore - .register(keycloak, keycloakConf) - .register(fastifySwagger, { transformObject: () => openApiDocument }) - .register(fastifySwaggerUi, swaggerUiConf) - .register(apiRouter()) - .addHook('onRoute', (opts) => { - if (opts.path === `${apiPrefix}/healthz`) { - opts.logLevel = 'silent' - } - }) - .setErrorHandler((error: Error, req: FastifyRequest, reply) => { - const statusCode = 500 - // @ts-ignore vérifier l'objet - const message = error.description || error.message - reply.status(statusCode).send({ status: statusCode, error: message, stack: error.stack }) - log('info', { reqId: req.id, error }) - }) - .addHook('onResponse', (req, res) => { - if (res.statusCode < 400) { - req.log.info({ status: res.statusCode, userId: req.session?.user?.id }) - } else if (res.statusCode < 500) { - req.log.warn({ status: res.statusCode, userId: req.session?.user?.id }) - } else { - req.log.error({ status: res.statusCode, userId: req.session?.user?.id }) - } - }) + app: any; + logger: any; -await app.ready() + async init() { + const contract = await getContract(); + this.app = fastify(fastifyConf) + .register(helmet, () => ({ + contentSecurityPolicy: !(isInt || isDev || isTest), + })) + .register(fastifyCookie) + .register(fastifySession, sessionConf) + // @ts-ignore + .register(keycloak, keycloakConf) + .register(fastifySwagger, { + transformObject: () => + generateOpenApi(contract, swaggerConf, { + setOperationId: true, + }), + }) + .register(fastifySwaggerUi, swaggerUiConf) + .register(apiRouter()) + .addHook('onRoute', (opts) => { + if (opts.path === `${apiPrefix}/healthz`) { + opts.logLevel = 'silent'; + } + }) + .setErrorHandler((error: Error, req: FastifyRequest, reply) => { + const statusCode = 500; + // @ts-ignore vérifier l'objet + const message = error.description || error.message; + reply.status(statusCode).send({ + status: statusCode, + error: message, + stack: error.stack, + }); + log('info', { reqId: req.id, error }); + }) + .addHook('onResponse', (req, res) => { + if (res.statusCode < 400) { + req.log.info({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } else if (res.statusCode < 500) { + req.log.warn({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } else { + req.log.error({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } + }); + this.logger = this.app.log as CustomLogger; -export const logger = app.log as CustomLogger -export default app + await this.app.ready(); + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts index d74608f8e..9af7a8734 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts @@ -1,61 +1,81 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { PrismaClientInitializationError } from '@prisma/client/runtime/library.js' -import prisma from './__mocks__/prisma.js' -import app, { logger } from './app.js' -import { getConnection } from './connect.js' - -vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) -vi.mock('@old-server/resources/queries-index.js') -vi.mock('./models/log.js', () => getModel('getLogModel')) -vi.mock('./models/repository.js', () => getModel('getRepositoryModel')) -vi.mock('./models/permission.js', () => getModel('getPermissionModel')) -vi.mock('./models/environment.js', () => getModel('getEnvironmentModel')) -vi.mock('./models/project.js', () => getModel('getProjectModel')) -vi.mock('./models/user.js', () => getModel('getUserModel')) -vi.mock('./models/users-projects.js', () => getModel('getRolesModel')) -vi.mock('./models/zone.js', () => getModel('getZoneModel')) -vi.mock('./prisma.js') - -vi.spyOn(app, 'listen') -vi.spyOn(logger, 'info') -vi.spyOn(logger, 'warn') -vi.spyOn(logger, 'error') -vi.spyOn(logger, 'debug') +import { PrismaClientInitializationError } from '@prisma/client/runtime/library.js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import prisma from './__mocks__/prisma.js'; +import app, { logger } from './app.js'; +import { getConnection } from './connect.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('./utils/mocks.js')).mockSessionPlugin, +); +vi.mock('@old-server/resources/queries-index.js'); +vi.mock('./models/log.js', () => getModel('getLogModel')); +vi.mock('./models/repository.js', () => getModel('getRepositoryModel')); +vi.mock('./models/permission.js', () => getModel('getPermissionModel')); +vi.mock('./models/environment.js', () => getModel('getEnvironmentModel')); +vi.mock('./models/project.js', () => getModel('getProjectModel')); +vi.mock('./models/user.js', () => getModel('getUserModel')); +vi.mock('./models/users-projects.js', () => getModel('getRolesModel')); +vi.mock('./models/zone.js', () => getModel('getZoneModel')); +vi.mock('./prisma.js'); + +vi.spyOn(app, 'listen'); +vi.spyOn(logger, 'info'); +vi.spyOn(logger, 'warn'); +vi.spyOn(logger, 'error'); +vi.spyOn(logger, 'debug'); function getModel(modelName) { - return { - [modelName]: vi.fn(() => ({ - sync: vi.fn(), - hasMany: vi.fn(), - belongsTo: vi.fn(), - belongsToMany: vi.fn(), - })), - } + return { + [modelName]: vi.fn(() => ({ + sync: vi.fn(), + hasMany: vi.fn(), + belongsTo: vi.fn(), + belongsToMany: vi.fn(), + })), + }; } describe('connect', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should connect to postgres', async () => { - await getConnection() - - expect(logger.info.mock.calls).toHaveLength(2) - expect(logger.info.mock.calls).toContainEqual([`Trying to connect to Postgres with: ${process.env.DB_URL}`]) - expect(logger.info.mock.calls).toContainEqual(['Connected to Postgres!']) - }) - - it('should fail to connect once, then connect to postgres', async () => { - const errorToCatch = new PrismaClientInitializationError('Failed to connect', '2.19.0', 'P1001') - - prisma.$connect.mockRejectedValueOnce(errorToCatch) - await getConnection() - - expect(logger.info.mock.calls).toHaveLength(5) - expect(logger.info.mock.calls).toContainEqual([`Trying to connect to Postgres with: ${process.env.DB_URL}`]) - expect(logger.info.mock.calls).toContainEqual(['Could not connect to Postgres: Failed to connect']) - expect(logger.info.mock.calls).toContainEqual(['Retrying (4 tries left)']) - expect(logger.info.mock.calls).toContainEqual(['Connected to Postgres!']) - }) -}) + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should connect to postgres', async () => { + await getConnection(); + + expect(logger.info.mock.calls).toHaveLength(2); + expect(logger.info.mock.calls).toContainEqual([ + `Trying to connect to Postgres with: ${process.env.DB_URL}`, + ]); + expect(logger.info.mock.calls).toContainEqual([ + 'Connected to Postgres!', + ]); + }); + + it('should fail to connect once, then connect to postgres', async () => { + const errorToCatch = new PrismaClientInitializationError( + 'Failed to connect', + '2.19.0', + 'P1001', + ); + + prisma.$connect.mockRejectedValueOnce(errorToCatch); + await getConnection(); + + expect(logger.info.mock.calls).toHaveLength(5); + expect(logger.info.mock.calls).toContainEqual([ + `Trying to connect to Postgres with: ${process.env.DB_URL}`, + ]); + expect(logger.info.mock.calls).toContainEqual([ + 'Could not connect to Postgres: Failed to connect', + ]); + expect(logger.info.mock.calls).toContainEqual([ + 'Retrying (4 tries left)', + ]); + expect(logger.info.mock.calls).toContainEqual([ + 'Connected to Postgres!', + ]); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts index c9e29b8fa..69d2bcb7b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts @@ -1,28 +1,38 @@ // @ts-nocheck - /** * How to use ? * npx vite-node src/init/db/dump.ts * format ./data.ts with linter * cut/paste to packages/test-utils/src/imports/data.ts */ +import prisma from '@old-server/prisma.js'; +import { Prisma } from '@prisma/client'; +import { writeFileSync } from 'node:fs'; -import { writeFileSync } from 'node:fs' -import { Prisma } from '@prisma/client' -import { associations, manyToManyRelation, modelKeys, models, resourceListToDict } from './utils.js' -import prisma from '@old-server/prisma.js' +import { + associations, + manyToManyRelation, + modelKeys, + models, + resourceListToDict, +} from './utils.js'; -const Models = resourceListToDict(Prisma.dmmf.datamodel.models) +const Models = resourceListToDict(Prisma.dmmf.datamodel.models); for (const modelKey of modelKeys) { - const modelDatas = await prisma[modelKey].findMany() - models[modelKey] = modelDatas + const modelDatas = await prisma[modelKey].findMany(); + models[modelKey] = modelDatas; } for (const [model, targetModel, relationKey] of manyToManyRelation) { - const modelKey = model.slice(0, 1).toLocaleLowerCase() + model.slice(1) - const modelDatas = await prisma[modelKey].findMany({ select: { [Models[model].id]: true, [relationKey]: { select: { [Models[targetModel].id]: true } } } }) - associations.push([modelKey, modelDatas]) + const modelKey = model.slice(0, 1).toLocaleLowerCase() + model.slice(1); + const modelDatas = await prisma[modelKey].findMany({ + select: { + [Models[model].id]: true, + [relationKey]: { select: { [Models[targetModel].id]: true } }, + }, + }); + associations.push([modelKey, modelDatas]); } -const a = JSON.stringify({ ...models, associations }, null, 2) +const a = JSON.stringify({ ...models, associations }, null, 2); -writeFileSync('./data.ts', `export const data = ${a}`) +writeFileSync('./data.ts', `export const data = ${a}`); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts index b834f824d..c57d6706f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts @@ -1,51 +1,58 @@ -import { modelKeys } from './utils.js' -import { logger } from '@old-server/app.js' -import prisma from '@old-server/prisma.js' +import { logger } from '@old-server/app.js'; +import prisma from '@old-server/prisma.js'; + +import { modelKeys } from './utils.js'; type ExtractKeysWithFields = { - [K in keyof T]: T[K] extends { fields: any } ? K : never -}[keyof T] + [K in keyof T]: T[K] extends { fields: any } ? K : never; +}[keyof T]; -type Models = ExtractKeysWithFields +type Models = ExtractKeysWithFields; type Imports = Partial> & { - associations: [Models, any[]] -} + associations: [Models, any[]]; +}; export async function initDb(data: Imports) { - const dataStringified = JSON.stringify(data) - const dataParsed = JSON.parse(dataStringified, (key, value) => { - try { - if (['permissions', 'everyonePerms'].includes(key)) { - return BigInt(value.slice(0, value.length - 1)) - } - } catch (_error) { - return value + const dataStringified = JSON.stringify(data); + const dataParsed = JSON.parse(dataStringified, (key, value) => { + try { + if (['permissions', 'everyonePerms'].includes(key)) { + return BigInt(value.slice(0, value.length - 1)); + } + } catch (_error) { + return value; + } + return value; + }); + logger.info('Drop tables'); + for (const modelKey of modelKeys.toReversed()) { + // @ts-ignore + await prisma[modelKey].deleteMany(); + } + logger.info('Import models'); + for (const modelKey of modelKeys) { + // @ts-ignore + await prisma[modelKey].createMany({ data: dataParsed[modelKey] }); } - return value - }) - logger.info('Drop tables') - for (const modelKey of modelKeys.toReversed()) { - // @ts-ignore - await prisma[modelKey].deleteMany() - } - logger.info('Import models') - for (const modelKey of modelKeys) { - // @ts-ignore - await prisma[modelKey].createMany({ data: dataParsed[modelKey] }) - } - logger.info('Import associations') - for (const [modelKey, rows] of dataParsed.associations) { - for (const row of rows) { - const idKey = 'id' - const connectKeys = Object.keys(row).filter(key => key !== idKey) - const dataConnects = connectKeys.reduce((acc, curr) => { - acc[curr] = { connect: row[curr] } - return acc - }, {} as Record) - // @ts-ignore - await prisma[modelKey].update({ where: { id: row.id }, data: dataConnects }) + logger.info('Import associations'); + for (const [modelKey, rows] of dataParsed.associations) { + for (const row of rows) { + const idKey = 'id'; + const connectKeys = Object.keys(row).filter((key) => key !== idKey); + const dataConnects = connectKeys.reduce( + (acc, curr) => { + acc[curr] = { connect: row[curr] }; + return acc; + }, + {} as Record, + ); + // @ts-ignore + await prisma[modelKey].update({ + where: { id: row.id }, + data: dataConnects, + }); + } } - } - logger.info('End import') + logger.info('End import'); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts index 9b831ee08..b603e7073 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts @@ -1,52 +1,53 @@ -import { describe, expect, it, vi } from 'vitest' -import prisma from '../../__mocks__/prisma.js' -import { modelKeys, moveBefore, resourceListToDict } from './utils.js' +import { describe, expect, it, vi } from 'vitest'; -vi.mock('fs', () => ({ writeFileSync: vi.fn() })) +import prisma from '../../__mocks__/prisma.js'; +import { modelKeys, moveBefore, resourceListToDict } from './utils.js'; + +vi.mock('fs', () => ({ writeFileSync: vi.fn() })); for (const modelKey of modelKeys) { - prisma[modelKey].findMany.mockResolvedValue([]) + prisma[modelKey].findMany.mockResolvedValue([]); } describe('test moveBefore', () => { - it('should be moved', () => { - const arr = ['a', 'b', 'c'] - const arrSorted = moveBefore(arr, 'c', 'b') - expect(arrSorted).toEqual(['a', 'c', 'b']) - - const arrSorted2 = moveBefore(arr, 'c', 'a') - expect(arrSorted2).toEqual(['c', 'a', 'b']) - }) - it('should not be moved', () => { - const arr = ['a', 'b', 'c'] - const arrSorted = moveBefore(arr, 'b', 'c') - expect(arrSorted).toEqual(false) - - const arrSorted2 = moveBefore(arr, 'a', 'c') - expect(arrSorted2).toEqual(false) - - const arrSorted3 = moveBefore(arr, 'c', 'c') - expect(arrSorted3).toEqual(false) - }) -}) + it('should be moved', () => { + const arr = ['a', 'b', 'c']; + const arrSorted = moveBefore(arr, 'c', 'b'); + expect(arrSorted).toEqual(['a', 'c', 'b']); + + const arrSorted2 = moveBefore(arr, 'c', 'a'); + expect(arrSorted2).toEqual(['c', 'a', 'b']); + }); + it('should not be moved', () => { + const arr = ['a', 'b', 'c']; + const arrSorted = moveBefore(arr, 'b', 'c'); + expect(arrSorted).toEqual(false); + + const arrSorted2 = moveBefore(arr, 'a', 'c'); + expect(arrSorted2).toEqual(false); + + const arrSorted3 = moveBefore(arr, 'c', 'c'); + expect(arrSorted3).toEqual(false); + }); +}); it('test resourceListToDict (by name)', () => { - const list = [ - { name: 'a', value: 1 }, - { name: 'b', value: 2 }, - { name: 'c', value: 3 }, - ] - const dict = resourceListToDict(list) - expect(dict).toEqual({ - a: { name: 'a', value: 1 }, - b: { name: 'b', value: 2 }, - c: { name: 'c', value: 3 }, - }) -}) + const list = [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 }, + { name: 'c', value: 3 }, + ]; + const dict = resourceListToDict(list); + expect(dict).toEqual({ + a: { name: 'a', value: 1 }, + b: { name: 'b', value: 2 }, + c: { name: 'c', value: 3 }, + }); +}); it('stringify bigint', () => { - const list = { name: 'a', value: 1n } + const list = { name: 'a', value: 1n }; - const dict = JSON.stringify(list) + const dict = JSON.stringify(list); - expect(dict).toEqual('{"name":"a","value":"1n"}') -}) + expect(dict).toEqual('{"name":"a","value":"1n"}'); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts index 95924751a..941a10fa3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts @@ -1,85 +1,107 @@ // @ts-nocheck -import { Prisma } from '@prisma/client' +import { Prisma } from '@prisma/client'; // eslint-disable-next-line no-extend-native BigInt.prototype.toJSON = function () { - return `${this.toString()}n` -} + return `${this.toString()}n`; +}; -export type ResourceByName = Record -export function resourceListToDict(resList: Array): ResourceByName { - return resList.reduce((acc, curr) => { - return { - ...acc, - [curr.name]: curr, - } - }, {} as ResourceByName) +export type ResourceByName< + T extends { + name: string; + }, +> = Record; +export function resourceListToDict( + resList: Array, +): ResourceByName { + return resList.reduce( + (acc, curr) => { + return { + ...acc, + [curr.name]: curr, + }; + }, + {} as ResourceByName, + ); } // @ts-ignore -const Models = resourceListToDict(Prisma.dmmf.datamodel.models) -let ModelsNames = Object.keys(Models) -let ModelsOrder = [...ModelsNames] +const Models = resourceListToDict(Prisma.dmmf.datamodel.models); +let ModelsNames = Object.keys(Models); +let ModelsOrder = [...ModelsNames]; -export function moveBefore(arr: T, toMove: T[number], ref: T[number]): T | false { - const iref = arr.indexOf(ref) - const moveref = arr.indexOf(toMove) - if (moveref <= iref) return false - return [ - ...arr.slice(0, iref), - arr[moveref], - ...arr.slice(iref, moveref), - ...arr.slice(moveref + 1), - ] as T +export function moveBefore( + arr: T, + toMove: T[number], + ref: T[number], +): T | false { + const iref = arr.indexOf(ref); + const moveref = arr.indexOf(toMove); + if (moveref <= iref) return false; + return [ + ...arr.slice(0, iref), + arr[moveref], + ...arr.slice(iref, moveref), + ...arr.slice(moveref + 1), + ] as T; } -export const manyToManyRelation: [string, string, string][] = [] +export const manyToManyRelation: [string, string, string][] = []; function sort() { - let hasChanged = false - for (const model of ModelsNames) { - for (const field of Models[model].fields) { - if (field.isId) Models[model].id = field.name - if (field.type in Models) { - const relationField = Models[field.type].fields.find(({ type }) => type === model) - if (!relationField) throw new Error('unable to find matching model') - if ( - (relationField.isRequired && field.isRequired && !relationField.isList) - || (relationField.isRequired && !field.isRequired) - ) { - const moveRes = moveBefore(ModelsOrder, model, field.type) - if (moveRes) { - hasChanged = true - ModelsOrder = moveRes - } - } - if ( - field.isList && relationField.isList - && !manyToManyRelation.find(test => - (test[0] === model && test[1] === field.type) || (test[0] === field.type && test[1] === model)) - ) { - manyToManyRelation.push([model, field.type, field.name]) + let hasChanged = false; + for (const model of ModelsNames) { + for (const field of Models[model].fields) { + if (field.isId) Models[model].id = field.name; + if (field.type in Models) { + const relationField = Models[field.type].fields.find( + ({ type }) => type === model, + ); + if (!relationField) + throw new Error('unable to find matching model'); + if ( + (relationField.isRequired && + field.isRequired && + !relationField.isList) || + (relationField.isRequired && !field.isRequired) + ) { + const moveRes = moveBefore(ModelsOrder, model, field.type); + if (moveRes) { + hasChanged = true; + ModelsOrder = moveRes; + } + } + if ( + field.isList && + relationField.isList && + !manyToManyRelation.find( + (test) => + (test[0] === model && test[1] === field.type) || + (test[0] === field.type && test[1] === model), + ) + ) { + manyToManyRelation.push([model, field.type, field.name]); + } + } } - } } - } - ModelsNames = ModelsOrder - if (hasChanged) sort() + ModelsNames = ModelsOrder; + if (hasChanged) sort(); } -sort() +sort(); // special case to study -const logUserCase = moveBefore(ModelsOrder, 'User', 'Log') +const logUserCase = moveBefore(ModelsOrder, 'User', 'Log'); if (logUserCase) { - ModelsOrder = logUserCase + ModelsOrder = logUserCase; } -const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log') +const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log'); if (logProjectCase) { - ModelsOrder = logProjectCase + ModelsOrder = logProjectCase; } -export const models: Record = {} -export const associations: Record = [] -export const modelKeys = ModelsOrder.map(model => model.slice(0, 1).toLocaleLowerCase() + model.slice(1)) +export const models: Record = {}; +export const associations: Record = []; +export const modelKeys = ModelsOrder.map( + (model) => model.slice(0, 1).toLocaleLowerCase() + model.slice(1), +); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts index 075578c96..9c88b20e7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts @@ -1,14 +1,14 @@ -import type { PrismaClient } from '@prisma/client' -import { beforeEach, vi } from 'vitest' -import { mockDeep, mockReset } from 'vitest-mock-extended' +import type { PrismaClient } from '@prisma/client'; +import { beforeEach, vi } from 'vitest'; +import { mockDeep, mockReset } from 'vitest-mock-extended'; -vi.mock('../prisma.js') +vi.mock('../prisma.js'); -const prisma = mockDeep() +const prisma = mockDeep(); beforeEach(() => { - // reset les mocks - mockReset(prisma) -}) + // reset les mocks + mockReset(prisma); +}); -export default prisma +export default prisma; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts index 3e9556625..2fd1aab81 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts @@ -1,24 +1,24 @@ -import fp from 'fastify-plugin' -import type { User } from '@cpn-console/test-utils' +import type { User } from '@cpn-console/test-utils'; +import fp from 'fastify-plugin'; -let requestor: User +let requestor: User; export function setRequestor(user: User) { - requestor = user + requestor = user; } export function getRequestor() { - return requestor + return requestor; } export async function mockSessionPlugin() { - const sessionPlugin = (app, opt, next) => { - app.addHook('onRequest', (req, res, next) => { - req.session = { user: getRequestor() } - next() - }) - next() - } + const sessionPlugin = (app, opt, next) => { + app.addHook('onRequest', (req, res, next) => { + req.session = { user: getRequestor() }; + next(); + }); + next(); + }; - return { default: fp(sessionPlugin) } + return { default: fp(sessionPlugin) }; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts index 9e9f245ce..7ec0df5b0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts @@ -1,46 +1,56 @@ -import { readdirSync, statSync } from 'node:fs' -import { type Plugin, pluginManager } from '@cpn-console/hooks' -import { plugin as argo } from '@cpn-console/argocd-plugin' -import { plugin as gitlab } from '@cpn-console/gitlab-plugin' -import { plugin as harbor } from '@cpn-console/harbor-plugin' -import { plugin as keycloak } from '@cpn-console/keycloak-plugin' -import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin' -import { plugin as nexus } from '@cpn-console/nexus-plugin' -import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin' -import { plugin as vault } from '@cpn-console/vault-plugin' -import { pluginManagerOptions } from './utils/plugins.js' -import { pluginsDir } from './utils/env.js' +import { plugin as argo } from '@cpn-console/argocd-plugin'; +import { plugin as gitlab } from '@cpn-console/gitlab-plugin'; +import { plugin as harbor } from '@cpn-console/harbor-plugin'; +import { type Plugin, pluginManager } from '@cpn-console/hooks'; +import { plugin as keycloak } from '@cpn-console/keycloak-plugin'; +import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin'; +import { plugin as nexus } from '@cpn-console/nexus-plugin'; +import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin'; +import { plugin as vault } from '@cpn-console/vault-plugin'; +import { readdirSync, statSync } from 'node:fs'; + +import { pluginsDir } from './utils/env.js'; +import { pluginManagerOptions } from './utils/plugins.js'; export async function initPm() { - const pm = pluginManager(pluginManagerOptions) - pm.register(argo) - pm.register(gitlab) - pm.register(harbor) - pm.register(keycloak) - pm.register(kubernetes) - pm.register(nexus) - pm.register(sonarqube) - pm.register(vault) + const pm = pluginManager(pluginManagerOptions); + pm.register(argo); + pm.register(gitlab); + pm.register(harbor); + pm.register(keycloak); + pm.register(kubernetes); + pm.register(nexus); + pm.register(sonarqube); + pm.register(vault); - if (!statSync(pluginsDir, { - throwIfNoEntry: false, - })) { - return pm - } - for (const dirName of readdirSync(pluginsDir)) { - const moduleAbsPath = `${pluginsDir}/${dirName}` - try { - statSync(`${moduleAbsPath}/package.json`) - const pkg = await import(`${moduleAbsPath}/package.json`, { with: { type: 'json' } }) - const entrypoint = pkg.default.module || pkg.default.main - if (!entrypoint) throw new Error(`No entrypoint found in package.json : ${pkg.default.name}`) - const { plugin } = await import(`${moduleAbsPath}/${entrypoint}`) as { plugin: Plugin } - pm.register(plugin) - } catch (error) { - console.error(`Could not import module ${moduleAbsPath}`) - console.error(error.stack) + if ( + !statSync(pluginsDir, { + throwIfNoEntry: false, + }) + ) { + return pm; + } + for (const dirName of readdirSync(pluginsDir)) { + const moduleAbsPath = `${pluginsDir}/${dirName}`; + try { + statSync(`${moduleAbsPath}/package.json`); + const pkg = await import(`${moduleAbsPath}/package.json`, { + with: { type: 'json' }, + }); + const entrypoint = pkg.default.module || pkg.default.main; + if (!entrypoint) + throw new Error( + `No entrypoint found in package.json : ${pkg.default.name}`, + ); + const { plugin } = (await import( + `${moduleAbsPath}/${entrypoint}` + )) as { plugin: Plugin }; + pm.register(plugin); + } catch (error) { + console.error(`Could not import module ${moduleAbsPath}`); + console.error(error.stack); + } } - } - return pm + return pm; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts index 292cbdb29..4608568f7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts @@ -1,70 +1,75 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { getPreparedApp } from './prepare-app.js' -import { getConnection } from './connect.js' -import { initDb } from './init/db/index.js' -import app, { logger } from './app.js' +import { beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) -vi.mock('./connect.js') -vi.mock('./index.js') -vi.mock('./utils/logger.js') -vi.mock('./init/db/index.js', () => ({ initDb: vi.fn() })) +import app, { logger } from './app.js'; +import { getConnection } from './connect.js'; +import { initDb } from './init/db/index.js'; +import { getPreparedApp } from './prepare-app.js'; -vi.spyOn(app, 'listen') -vi.spyOn(logger, 'info') -vi.spyOn(logger, 'warn') -vi.spyOn(logger, 'error') -vi.spyOn(logger, 'debug') +vi.mock( + 'fastify-keycloak-adapter', + (await import('./utils/mocks.js')).mockSessionPlugin, +); +vi.mock('./connect.js'); +vi.mock('./index.js'); +vi.mock('./utils/logger.js'); +vi.mock('./init/db/index.js', () => ({ initDb: vi.fn() })); + +vi.spyOn(app, 'listen'); +vi.spyOn(logger, 'info'); +vi.spyOn(logger, 'warn'); +vi.spyOn(logger, 'error'); +vi.spyOn(logger, 'debug'); describe('server', () => { - beforeEach(() => { - vi.clearAllMocks() - }) + beforeEach(() => { + vi.clearAllMocks(); + }); - it('should getConnection', async () => { - // const port = Math.round(Math.random() * 10000) + 1024 - await getPreparedApp().catch(err => console.warn(err)) + it('should getConnection', async () => { + // const port = Math.round(Math.random() * 10000) + 1024 + await getPreparedApp().catch((err) => console.warn(err)); - expect(getConnection).toHaveBeenCalledTimes(1) - expect(initDb.mock.calls).toHaveLength(1) - }) + expect(getConnection).toHaveBeenCalledTimes(1); + expect(initDb.mock.calls).toHaveLength(1); + }); - it('should throw an error on connection to DB', async () => { - const error = new Error('This is OK!') - getConnection.mockRejectedValueOnce(error) + it('should throw an error on connection to DB', async () => { + const error = new Error('This is OK!'); + getConnection.mockRejectedValueOnce(error); - let response - await getPreparedApp() - .catch((err) => { response = err }) + let response; + await getPreparedApp().catch((err) => { + response = err; + }); - expect(getConnection.mock.calls).toHaveLength(1) - expect(app.listen.mock.calls).toHaveLength(0) - expect(response).toMatchObject(error) - }) + expect(getConnection.mock.calls).toHaveLength(1); + expect(app.listen.mock.calls).toHaveLength(0); + expect(response).toMatchObject(error); + }); - it('should throw an error on initDb import if module is not found', async () => { - const error = new Error('Failed to load') - initDb.mockRejectedValueOnce(error) + it('should throw an error on initDb import if module is not found', async () => { + const error = new Error('Failed to load'); + initDb.mockRejectedValueOnce(error); - await getPreparedApp() + await getPreparedApp(); - expect(initDb.mock.calls).toHaveLength(1) - expect(logger.info.mock.calls).toHaveLength(3) - }) + expect(initDb.mock.calls).toHaveLength(1); + expect(logger.info.mock.calls).toHaveLength(3); + }); - it('should throw an error on initDb import', async () => { - const error = new Error('This is OK!') - initDb.mockRejectedValueOnce(error) + it('should throw an error on initDb import', async () => { + const error = new Error('This is OK!'); + initDb.mockRejectedValueOnce(error); - let response - try { - await getPreparedApp() - } catch (err) { - response = err - } + let response; + try { + await getPreparedApp(); + } catch (err) { + response = err; + } - expect(initDb.mock.calls).toHaveLength(1) - expect(logger.info.mock.calls).toHaveLength(2) - expect(response).toMatchObject(error) - }) -}) + expect(initDb.mock.calls).toHaveLength(1); + expect(logger.info.mock.calls).toHaveLength(2); + expect(response).toMatchObject(error); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts index 4590932b6..4e54f7a77 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts @@ -1,5 +1,5 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient() +const prisma = new PrismaClient(); -export default prisma +export default prisma; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts index 9174f544e..47fef06eb 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts @@ -1,183 +1,219 @@ -import { describe, expect, it } from 'vitest' -import type { AdminRole, User } from '@prisma/client' -import { faker } from '@faker-js/faker' -import prisma from '../../__mocks__/prisma.js' -import { BadRequest400 } from '../../utils/errors.ts' -import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business.ts' +import { faker } from '@faker-js/faker'; +import type { AdminRole, User } from '@prisma/client'; +import { describe, expect, it } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import { BadRequest400 } from '../../utils/errors.ts'; +import { + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from './business.ts'; describe('test admin-role business', () => { - describe('listRoles', () => { - it('should stringify bigint', async () => { - const partialRole: Partial = { - permissions: 4n, - } - - prisma.adminRole.findMany.mockResolvedValueOnce([partialRole]) - const response = await listRoles() - expect(response).toEqual([{ permissions: '4' }]) - }) - }) - - describe('createRole', () => { - it('should create role with incremented position when position 0 is the highest', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 0, - } - - prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole) - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.adminRole.create.mockResolvedValue(null) - await createRole({ name: 'test' }) - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 1 } }) - }) - - it('should create role with incremented position with bigger position', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - } - - prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole) - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.adminRole.create.mockResolvedValue(null) - await createRole({ name: 'test' }) - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 51 } }) - }) - - it('should create role with incremented position with no role in db', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - } - - prisma.adminRole.findFirst.mockResolvedValueOnce(undefined) - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.adminRole.create.mockResolvedValue(null) - await createRole({ name: 'test' }) - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 0 } }) - }) - }) - describe('deleteRole', () => { - const roleId = faker.string.uuid() - it('should delete role and remove id from concerned users', async () => { - const users = [{ - adminRoleIds: [roleId], - id: faker.string.uuid(), - }, { - adminRoleIds: [roleId, faker.string.uuid()], - id: faker.string.uuid(), - }] as const satisfies Partial[] - - prisma.user.findMany.mockResolvedValueOnce(users) - prisma.adminRole.findMany.mockResolvedValueOnce([]) - prisma.adminRole.create.mockResolvedValue(null) - await deleteRole(roleId) - - expect(prisma.user.update).toHaveBeenNthCalledWith(1, { where: { id: users[0].id }, data: { adminRoleIds: [] } }) - expect(prisma.user.update).toHaveBeenNthCalledWith(2, { where: { id: users[1].id }, data: { adminRoleIds: [users[1].adminRoleIds[1]] } }) - expect(prisma.adminRole.delete).toHaveBeenCalledWith({ where: { id: roleId } }) - }) - }) - describe('countRolesMembers', () => { - it('should return aggregated role member counts', async () => { - const partialRoles = [{ - id: faker.string.uuid(), - }, { - id: faker.string.uuid(), - }] as const satisfies Partial[] - - const users = [{ - adminRoleIds: [partialRoles[0].id, partialRoles[1].id], - }, { - adminRoleIds: [partialRoles[1].id], - }] as const satisfies Partial[] - prisma.adminRole.findMany.mockResolvedValue(partialRoles) - prisma.user.findMany.mockResolvedValue(users) - - const response = await countRolesMembers() - - expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 }) - }) - }) - describe('patchRoles', () => { - const dbRoles: AdminRole[] = [{ - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 0, - }, { - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 1, - }] - - it('should do nothing', async () => { - prisma.adminRole.findMany.mockResolvedValue([]) - await patchRoles([]) - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) - }) - - it('should return 400 if incoherent positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 1 }, - ] - prisma.adminRole.findMany.mockResolvedValue(dbRoles) - - const response = await patchRoles(updateRoles) - - expect(response).instanceOf(BadRequest400) - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) - }) - it('should return 400 if incoherent positions (missing roles)', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, position: 1 }, - ] - prisma.adminRole.findMany.mockResolvedValue(dbRoles) - - const response = await patchRoles(updateRoles) - - expect(response).instanceOf(BadRequest400) - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) - }) - it('should update positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 0 }, - ] - prisma.adminRole.findMany.mockResolvedValue(dbRoles) - - await patchRoles(updateRoles) - - expect(prisma.adminRole.update).toHaveBeenCalledTimes(2) - }) - it('should update permissions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, permissions: '0' }, - ] - prisma.adminRole.findMany.mockResolvedValue(dbRoles) - - await patchRoles(updateRoles) - - expect(prisma.adminRole.update).toHaveBeenCalledTimes(1) - expect(prisma.adminRole.update).toHaveBeenCalledWith({ - data: { - name: dbRoles[1].name, - oidcGroup: dbRoles[1].oidcGroup, - permissions: 0n, - position: 1, - }, - where: { - id: dbRoles[1].id, - }, - }) - }) - }) -}) + describe('listRoles', () => { + it('should stringify bigint', async () => { + const partialRole: Partial = { + permissions: 4n, + }; + + prisma.adminRole.findMany.mockResolvedValueOnce([partialRole]); + const response = await listRoles(); + expect(response).toEqual([{ permissions: '4' }]); + }); + }); + + describe('createRole', () => { + it('should create role with incremented position when position 0 is the highest', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 0, + }; + + prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole); + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]); + prisma.adminRole.create.mockResolvedValue(null); + await createRole({ name: 'test' }); + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ + data: { name: 'test', permissions: 0n, position: 1 }, + }); + }); + + it('should create role with incremented position with bigger position', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + }; + + prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole); + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]); + prisma.adminRole.create.mockResolvedValue(null); + await createRole({ name: 'test' }); + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ + data: { name: 'test', permissions: 0n, position: 51 }, + }); + }); + + it('should create role with incremented position with no role in db', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + }; + + prisma.adminRole.findFirst.mockResolvedValueOnce(undefined); + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]); + prisma.adminRole.create.mockResolvedValue(null); + await createRole({ name: 'test' }); + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ + data: { name: 'test', permissions: 0n, position: 0 }, + }); + }); + }); + describe('deleteRole', () => { + const roleId = faker.string.uuid(); + it('should delete role and remove id from concerned users', async () => { + const users = [ + { + adminRoleIds: [roleId], + id: faker.string.uuid(), + }, + { + adminRoleIds: [roleId, faker.string.uuid()], + id: faker.string.uuid(), + }, + ] as const satisfies Partial[]; + + prisma.user.findMany.mockResolvedValueOnce(users); + prisma.adminRole.findMany.mockResolvedValueOnce([]); + prisma.adminRole.create.mockResolvedValue(null); + await deleteRole(roleId); + + expect(prisma.user.update).toHaveBeenNthCalledWith(1, { + where: { id: users[0].id }, + data: { adminRoleIds: [] }, + }); + expect(prisma.user.update).toHaveBeenNthCalledWith(2, { + where: { id: users[1].id }, + data: { adminRoleIds: [users[1].adminRoleIds[1]] }, + }); + expect(prisma.adminRole.delete).toHaveBeenCalledWith({ + where: { id: roleId }, + }); + }); + }); + describe('countRolesMembers', () => { + it('should return aggregated role member counts', async () => { + const partialRoles = [ + { + id: faker.string.uuid(), + }, + { + id: faker.string.uuid(), + }, + ] as const satisfies Partial[]; + + const users = [ + { + adminRoleIds: [partialRoles[0].id, partialRoles[1].id], + }, + { + adminRoleIds: [partialRoles[1].id], + }, + ] as const satisfies Partial[]; + prisma.adminRole.findMany.mockResolvedValue(partialRoles); + prisma.user.findMany.mockResolvedValue(users); + + const response = await countRolesMembers(); + + expect(response).toEqual({ + [partialRoles[0].id]: 1, + [partialRoles[1].id]: 2, + }); + }); + }); + describe('patchRoles', () => { + const dbRoles: AdminRole[] = [ + { + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 0, + }, + { + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 1, + }, + ]; + + it('should do nothing', async () => { + prisma.adminRole.findMany.mockResolvedValue([]); + await patchRoles([]); + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0); + }); + + it('should return 400 if incoherent positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 1 }, + ]; + prisma.adminRole.findMany.mockResolvedValue(dbRoles); + + const response = await patchRoles(updateRoles); + + expect(response).instanceOf(BadRequest400); + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0); + }); + it('should return 400 if incoherent positions (missing roles)', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, position: 1 }, + ]; + prisma.adminRole.findMany.mockResolvedValue(dbRoles); + + const response = await patchRoles(updateRoles); + + expect(response).instanceOf(BadRequest400); + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0); + }); + it('should update positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 0 }, + ]; + prisma.adminRole.findMany.mockResolvedValue(dbRoles); + + await patchRoles(updateRoles); + + expect(prisma.adminRole.update).toHaveBeenCalledTimes(2); + }); + it('should update permissions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, permissions: '0' }, + ]; + prisma.adminRole.findMany.mockResolvedValue(dbRoles); + + await patchRoles(updateRoles); + + expect(prisma.adminRole.update).toHaveBeenCalledTimes(1); + expect(prisma.adminRole.update).toHaveBeenCalledWith({ + data: { + name: dbRoles[1].name, + oidcGroup: dbRoles[1].oidcGroup, + permissions: 0n, + position: 1, + }, + where: { + id: dbRoles[1].id, + }, + }); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts index 29bfb9067..3367eb615 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts @@ -1,90 +1,121 @@ -import type { Project, ProjectRole } from '@prisma/client' -import type { AdminRole, adminRoleContract } from '@cpn-console/shared' -import { - listAdminRoles, -} from '@old-server/resources/queries-index.js' -import type { ErrorResType } from '@old-server/utils/errors.js' -import { BadRequest400 } from '@old-server/utils/errors.js' -import prisma from '@old-server/prisma.js' +import type { AdminRole, adminRoleContract } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; +import { listAdminRoles } from '@old-server/resources/queries-index.js'; +import type { ErrorResType } from '@old-server/utils/errors.js'; +import { BadRequest400 } from '@old-server/utils/errors.js'; +import type { Project, ProjectRole } from '@prisma/client'; export async function listRoles() { - return listAdminRoles() - .then(roles => roles.map(role => ({ ...role, permissions: role.permissions.toString() }))) + return listAdminRoles().then((roles) => + roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + ); } -export async function patchRoles(roles: typeof adminRoleContract.patchAdminRoles.body._type): Promise { - const dbRoles = await prisma.adminRole.findMany() - const positionsAvailable: number[] = [] +export async function patchRoles( + roles: typeof adminRoleContract.patchAdminRoles.body._type, +): Promise { + const dbRoles = await prisma.adminRole.findMany(); + const positionsAvailable: number[] = []; - const updatedRoles: (Omit & { permissions: bigint })[] = dbRoles - .filter(dbRole => roles.find(role => role.id === dbRole.id)) // filter non concerned dbRoles - .map((dbRole) => { - const matchingRole = roles.find(role => role.id === dbRole.id) - if (typeof matchingRole?.position !== 'undefined' && !positionsAvailable.includes(matchingRole.position)) { - positionsAvailable.push(matchingRole.position) - } - return { - id: dbRole.id, - name: matchingRole?.name ?? dbRole.name, - permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : dbRole.permissions, - position: matchingRole?.position ?? dbRole.position, - oidcGroup: matchingRole?.oidcGroup ?? dbRole.oidcGroup, - } - }) + const updatedRoles: (Omit & { + permissions: bigint; + })[] = dbRoles + .filter((dbRole) => roles.find((role) => role.id === dbRole.id)) // filter non concerned dbRoles + .map((dbRole) => { + const matchingRole = roles.find((role) => role.id === dbRole.id); + if ( + typeof matchingRole?.position !== 'undefined' && + !positionsAvailable.includes(matchingRole.position) + ) { + positionsAvailable.push(matchingRole.position); + } + return { + id: dbRole.id, + name: matchingRole?.name ?? dbRole.name, + permissions: matchingRole?.permissions + ? BigInt(matchingRole?.permissions) + : dbRole.permissions, + position: matchingRole?.position ?? dbRole.position, + oidcGroup: matchingRole?.oidcGroup ?? dbRole.oidcGroup, + }; + }); - if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes') - for (const { id, ...role } of updatedRoles) { - await prisma.adminRole.update({ where: { id }, data: role }) - } + if ( + positionsAvailable.length && + positionsAvailable.length !== dbRoles.length + ) + return new BadRequest400( + 'Les numéros de position des rôles sont incohérentes', + ); + for (const { id, ...role } of updatedRoles) { + await prisma.adminRole.update({ where: { id }, data: role }); + } - return listRoles() + return listRoles(); } -export async function createRole(role: typeof adminRoleContract.createAdminRole.body._type) { - const dbMaxPosRole = (await prisma.adminRole.findFirst({ - orderBy: { position: 'desc' }, - select: { position: true }, - }))?.position ?? -1 +export async function createRole( + role: typeof adminRoleContract.createAdminRole.body._type, +) { + const dbMaxPosRole = + ( + await prisma.adminRole.findFirst({ + orderBy: { position: 'desc' }, + select: { position: true }, + }) + )?.position ?? -1; - await prisma.adminRole.create({ - data: { - ...role, - position: dbMaxPosRole + 1, - permissions: 0n, - }, - }) + await prisma.adminRole.create({ + data: { + ...role, + position: dbMaxPosRole + 1, + permissions: 0n, + }, + }); - return listRoles() + return listRoles(); } export async function countRolesMembers() { - const roles = await prisma.adminRole.findMany({ where: { oidcGroup: { equals: '' } }, select: { id: true } }) - const roleIds = roles.map(role => role.id) - const users = await prisma.user.findMany({ - where: { adminRoleIds: { hasSome: roleIds } }, - select: { adminRoleIds: true }, - }) - const rolesCounts: Record = Object.fromEntries(roles.map(role => [role.id, 0])) // {role uuid: 0} - for (const { adminRoleIds } of users) { - for (const roleId of adminRoleIds) { - rolesCounts[roleId]++ + const roles = await prisma.adminRole.findMany({ + where: { oidcGroup: { equals: '' } }, + select: { id: true }, + }); + const roleIds = roles.map((role) => role.id); + const users = await prisma.user.findMany({ + where: { adminRoleIds: { hasSome: roleIds } }, + select: { adminRoleIds: true }, + }); + const rolesCounts: Record = Object.fromEntries( + roles.map((role) => [role.id, 0]), + ); // {role uuid: 0} + for (const { adminRoleIds } of users) { + for (const roleId of adminRoleIds) { + rolesCounts[roleId]++; + } } - } - return rolesCounts + return rolesCounts; } export async function deleteRole(roleId: Project['id']) { - const allUsers = await prisma.user.findMany({ - where: { - adminRoleIds: { has: roleId }, - }, - }) - for (const user of allUsers) { - await prisma.user.update({ - where: { id: user.id }, - data: { adminRoleIds: user.adminRoleIds.filter(adminRoleId => adminRoleId !== roleId) }, - }) - } - await prisma.adminRole.delete({ where: { id: roleId } }) - return null + const allUsers = await prisma.user.findMany({ + where: { + adminRoleIds: { has: roleId }, + }, + }); + for (const user of allUsers) { + await prisma.user.update({ + where: { id: user.id }, + data: { + adminRoleIds: user.adminRoleIds.filter( + (adminRoleId) => adminRoleId !== roleId, + ), + }, + }); + } + await prisma.adminRole.delete({ where: { id: roleId } }); + return null; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts index 83fd52444..f09acde59 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts @@ -1,32 +1,38 @@ -import type { - AdminRole, - Prisma, -} from '@prisma/client' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { AdminRole, Prisma } from '@prisma/client'; -export const listAdminRoles = () => prisma.adminRole.findMany({ orderBy: { position: 'asc' } }) +export const listAdminRoles = () => + prisma.adminRole.findMany({ orderBy: { position: 'asc' } }); -export function createAdminRole(data: Pick) { - return prisma.adminRole.create({ - data: { - name: data.name, - permissions: 0n, - position: data.position, - }, - }) +export function createAdminRole( + data: Pick, +) { + return prisma.adminRole.create({ + data: { + name: data.name, + permissions: 0n, + position: data.position, + }, + }); } -export function updateAdminRole(id: AdminRole['id'], data: Pick) { - return prisma.projectRole.updateMany({ - where: { id }, - data, - }) +export function updateAdminRole( + id: AdminRole['id'], + data: Pick< + Prisma.AdminRoleUncheckedUpdateInput, + 'permissions' | 'name' | 'position' | 'id' + >, +) { + return prisma.projectRole.updateMany({ + where: { id }, + data, + }); } export function deleteAdminRole(id: AdminRole['id']) { - return prisma.projectRole.delete({ - where: { - id, - }, - }) + return prisma.projectRole.delete({ + where: { + id, + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts index 5fc0bc66c..6a9f67b59 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts @@ -1,181 +1,223 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { adminRoleContract } from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { BadRequest400 } from '../../utils/errors.js' -import { getUserMockInfos } from '../../utils/mocks.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListRolesMock = vi.spyOn(business, 'listRoles') -const businessCreateRoleMock = vi.spyOn(business, 'createRole') -const businessPatchRolesMock = vi.spyOn(business, 'patchRoles') -const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers') -const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole') +import { adminRoleContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessListRolesMock = vi.spyOn(business, 'listRoles'); +const businessCreateRoleMock = vi.spyOn(business, 'createRole'); +const businessPatchRolesMock = vi.spyOn(business, 'patchRoles'); +const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers'); +const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole'); describe('test adminRoleContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listAdminRoles', () => { - it('should return list of admin roles', async () => { - const roles = [{ id: faker.string.uuid(), name: 'Role 1', oidcGroup: '', position: 0, permissions: '1' }] - businessListRolesMock.mockResolvedValueOnce(roles) - - const response = await app.inject() - .get(adminRoleContract.listAdminRoles.path) - .end() - - expect(businessListRolesMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(roles) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('createAdminRole', () => { - it('should create a role for authorized users', async () => { - const user = getUserMockInfos(true) - const newRole = { id: 'newRole', name: 'New Role' } - const roleData = { name: 'New Role' } - - authUserMock.mockResolvedValueOnce(user) - businessCreateRoleMock.mockResolvedValueOnce(newRole) - - const response = await app.inject() - .post(adminRoleContract.createAdminRole.path) - .body(roleData) - .end() - - expect(businessCreateRoleMock).toHaveBeenCalledWith(roleData) - expect(response.json()).toEqual(newRole) - expect(response.statusCode).toEqual(201) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(adminRoleContract.createAdminRole.path) - .body({ name: 'New Role' }) - .end() - - expect(businessCreateRoleMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('patchAdminRoles', () => { - const updatedRoles = [{ id: faker.string.uuid(), name: 'Role 1', oidcGroup: '', position: 0, permissions: '1' }] - const rolesData = [{ id: updatedRoles[0].id, name: 'Updated Role' }] - it('should update roles for authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessPatchRolesMock.mockResolvedValueOnce(updatedRoles) - - const response = await app.inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end() - - expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData) - expect(response.json()).toEqual(updatedRoles) - expect(response.statusCode).toEqual(200) - }) - - it('should return error if business logic fails', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessPatchRolesMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - - const response = await app.inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end() - - expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData) - expect(response.statusCode).toEqual(400) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end() - - expect(businessPatchRolesMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('adminRoleMemberCounts', () => { - it('should return counts of role members for admin', async () => { - const user = getUserMockInfos(true) - const counts = { role1: 5, role2: 3 } - - authUserMock.mockResolvedValueOnce(user) - businessCountRolesMembersMock.mockResolvedValueOnce(counts) - - const response = await app.inject() - .get(adminRoleContract.adminRoleMemberCounts.path) - .end() - - expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(counts) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 if user is not admin', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(adminRoleContract.adminRoleMemberCounts.path) - .end() - - expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('deleteAdminRole', () => { - const roleId = faker.string.uuid() - it('should delete a role for authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessDeleteRoleMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .delete(adminRoleContract.deleteAdminRole.path.replace(':roleId', roleId)) - .end() - - expect(businessDeleteRoleMock).toHaveBeenCalledWith(roleId) - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(adminRoleContract.deleteAdminRole.path.replace(':roleId', roleId)) - .end() - - expect(businessDeleteRoleMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('listAdminRoles', () => { + it('should return list of admin roles', async () => { + const roles = [ + { + id: faker.string.uuid(), + name: 'Role 1', + oidcGroup: '', + position: 0, + permissions: '1', + }, + ]; + businessListRolesMock.mockResolvedValueOnce(roles); + + const response = await app + .inject() + .get(adminRoleContract.listAdminRoles.path) + .end(); + + expect(businessListRolesMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(roles); + expect(response.statusCode).toEqual(200); + }); + }); + + describe('createAdminRole', () => { + it('should create a role for authorized users', async () => { + const user = getUserMockInfos(true); + const newRole = { id: 'newRole', name: 'New Role' }; + const roleData = { name: 'New Role' }; + + authUserMock.mockResolvedValueOnce(user); + businessCreateRoleMock.mockResolvedValueOnce(newRole); + + const response = await app + .inject() + .post(adminRoleContract.createAdminRole.path) + .body(roleData) + .end(); + + expect(businessCreateRoleMock).toHaveBeenCalledWith(roleData); + expect(response.json()).toEqual(newRole); + expect(response.statusCode).toEqual(201); + }); + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(adminRoleContract.createAdminRole.path) + .body({ name: 'New Role' }) + .end(); + + expect(businessCreateRoleMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('patchAdminRoles', () => { + const updatedRoles = [ + { + id: faker.string.uuid(), + name: 'Role 1', + oidcGroup: '', + position: 0, + permissions: '1', + }, + ]; + const rolesData = [{ id: updatedRoles[0].id, name: 'Updated Role' }]; + it('should update roles for authorized users', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessPatchRolesMock.mockResolvedValueOnce(updatedRoles); + + const response = await app + .inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end(); + + expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData); + expect(response.json()).toEqual(updatedRoles); + expect(response.statusCode).toEqual(200); + }); + + it('should return error if business logic fails', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessPatchRolesMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + + const response = await app + .inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end(); + + expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData); + expect(response.statusCode).toEqual(400); + }); + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end(); + + expect(businessPatchRolesMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('adminRoleMemberCounts', () => { + it('should return counts of role members for admin', async () => { + const user = getUserMockInfos(true); + const counts = { role1: 5, role2: 3 }; + + authUserMock.mockResolvedValueOnce(user); + businessCountRolesMembersMock.mockResolvedValueOnce(counts); + + const response = await app + .inject() + .get(adminRoleContract.adminRoleMemberCounts.path) + .end(); + + expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(counts); + expect(response.statusCode).toEqual(200); + }); + + it('should return 403 if user is not admin', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(adminRoleContract.adminRoleMemberCounts.path) + .end(); + + expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('deleteAdminRole', () => { + const roleId = faker.string.uuid(); + it('should delete a role for authorized users', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessDeleteRoleMock.mockResolvedValueOnce(null); + + const response = await app + .inject() + .delete( + adminRoleContract.deleteAdminRole.path.replace( + ':roleId', + roleId, + ), + ) + .end(); + + expect(businessDeleteRoleMock).toHaveBeenCalledWith(roleId); + expect(response.statusCode).toEqual(204); + }); + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + adminRoleContract.deleteAdminRole.path.replace( + ':roleId', + roleId, + ), + ) + .end(); + + expect(businessDeleteRoleMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts index d69ea4a43..9d546453a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts @@ -1,74 +1,79 @@ -import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared' +import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; + import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from './business.js'; export function adminRoleRouter() { - return serverInstance.router(adminRoleContract, { - // Récupérer des projets - listAdminRoles: async () => { - const body = await listRoles() + return serverInstance.router(adminRoleContract, { + // Récupérer des projets + listAdminRoles: async () => { + const body = await listRoles(); - return { - status: 200, - body, - } - }, + return { + status: 200, + body, + }; + }, - createAdminRole: async ({ request: req, body }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + createAdminRole: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - const resBody = await createRole(body) + const resBody = await createRole(body); - return { - status: 201, - body: resBody, - } - }, + return { + status: 201, + body: resBody, + }; + }, - patchAdminRoles: async ({ request: req, body }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + patchAdminRoles: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - const resBody = await patchRoles(body) - if (resBody instanceof ErrorResType) return resBody + const resBody = await patchRoles(body); + if (resBody instanceof ErrorResType) return resBody; - return { - status: 200, - body: resBody, - } - }, + return { + status: 200, + body: resBody, + }; + }, - adminRoleMemberCounts: async ({ request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + adminRoleMemberCounts: async ({ request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - const resBody = await countRolesMembers() + const resBody = await countRolesMembers(); - return { - status: 200, - body: resBody, - } - }, + return { + status: 200, + body: resBody, + }; + }, - deleteAdminRole: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + deleteAdminRole: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - const resBody = await deleteRole(params.roleId) + const resBody = await deleteRole(params.roleId); - return { - status: 204, - body: resBody, - } - }, - }) + return { + status: 204, + body: resBody, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts index f9f338c64..be45b3024 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts @@ -1,73 +1,82 @@ -import { describe, expect, it } from 'vitest' -import type { AdminToken } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import prisma from '../../__mocks__/prisma.js' -import { createToken, deleteToken, listTokens } from './business.ts' +import type { AdminToken } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { describe, expect, it } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import { createToken, deleteToken, listTokens } from './business.ts'; describe('test admin-token business', () => { - describe('listTokens', () => { - it('should stringify bigint', async () => { - const partialtoken: Partial = { - permissions: 4n, - } + describe('listTokens', () => { + it('should stringify bigint', async () => { + const partialtoken: Partial = { + permissions: 4n, + }; - prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]) - const response = await listTokens({}) - expect(response).toEqual([{ permissions: '4' }]) - }) - it('should return revoked', async () => { - const partialtoken: Partial = { - permissions: 4n, - status: 'revoked', - } + prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]); + const response = await listTokens({}); + expect(response).toEqual([{ permissions: '4' }]); + }); + it('should return revoked', async () => { + const partialtoken: Partial = { + permissions: 4n, + status: 'revoked', + }; - prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]) - const response = await listTokens({ withRevoked: true }) - expect(response).toEqual([{ ...partialtoken, permissions: '4' }]) - }) - }) + prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]); + const response = await listTokens({ withRevoked: true }); + expect(response).toEqual([{ ...partialtoken, permissions: '4' }]); + }); + }); - describe('createToken', () => { - it('should create ', async () => { - const dbToken: Partial = undefined - const userId = faker.string.uuid() - const createdToken: AdminToken = { - expirationDate: null, - id: faker.string.uuid(), - name: 'test', - permissions: '2', - } - prisma.adminToken.findUnique.mockResolvedValueOnce(dbToken) - prisma.adminToken.create.mockResolvedValueOnce(createdToken) - await createToken({ name: 'test', permissions: '2', expirationDate: null }, userId, undefined) + describe('createToken', () => { + it('should create ', async () => { + const dbToken: Partial = undefined; + const userId = faker.string.uuid(); + const createdToken: AdminToken = { + expirationDate: null, + id: faker.string.uuid(), + name: 'test', + permissions: '2', + }; + prisma.adminToken.findUnique.mockResolvedValueOnce(dbToken); + prisma.adminToken.create.mockResolvedValueOnce(createdToken); + await createToken( + { name: 'test', permissions: '2', expirationDate: null }, + userId, + undefined, + ); - expect(prisma.adminToken.create).toHaveBeenCalledWith({ - data: { - name: 'test', - hash: expect.any(String), - permissions: 2n, - userId: expect.any(String), - expirationDate: undefined, - }, - omit: expect.any(Object), - include: { - owner: true, - }, - }) - }) - it('should not create cause expiration is too short', async () => { - const expirationDate = new Date() - await createToken({ name: 'test', permissions: '2', expirationDate: expirationDate.toISOString() }) + expect(prisma.adminToken.create).toHaveBeenCalledWith({ + data: { + name: 'test', + hash: expect.any(String), + permissions: 2n, + userId: expect.any(String), + expirationDate: undefined, + }, + omit: expect.any(Object), + include: { + owner: true, + }, + }); + }); + it('should not create cause expiration is too short', async () => { + const expirationDate = new Date(); + await createToken({ + name: 'test', + permissions: '2', + expirationDate: expirationDate.toISOString(), + }); - expect(prisma.adminToken.create).toHaveBeenCalledTimes(0) - }) - }) + expect(prisma.adminToken.create).toHaveBeenCalledTimes(0); + }); + }); - describe('deleteToken', () => { - it('should delete token', async () => { - prisma.adminToken.delete.mockResolvedValueOnce(undefined) - await deleteToken(faker.string.uuid()) - expect(prisma.adminToken.updateMany).toHaveBeenCalledTimes(1) - }) - }) -}) + describe('deleteToken', () => { + it('should delete token', async () => { + prisma.adminToken.delete.mockResolvedValueOnce(undefined); + await deleteToken(faker.string.uuid()); + expect(prisma.adminToken.updateMany).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts index 8acbb11e6..e99f6504a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts @@ -1,68 +1,90 @@ -import { createHash, randomUUID } from 'node:crypto' -import { type adminTokenContract, generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' -import type { $Enums, AdminToken, Prisma } from '@prisma/client' -import prisma from '../../prisma.js' -import { BadRequest400 } from '@old-server/utils/errors.js' +import { + type adminTokenContract, + generateRandomPassword, + isAtLeastTomorrow, +} from '@cpn-console/shared'; +import { BadRequest400 } from '@old-server/utils/errors.js'; +import type { $Enums, AdminToken, Prisma } from '@prisma/client'; +import { createHash, randomUUID } from 'node:crypto'; -export async function listTokens(query: typeof adminTokenContract.listAdminTokens.query._type) { - const where = { - status: { - in: ['active'] as $Enums.TokenStatus[], - }, - } as const satisfies Prisma.AdminTokenWhereInput +import prisma from '../../prisma.js'; - if (query?.withRevoked) where.status.in.push('revoked') +export async function listTokens( + query: typeof adminTokenContract.listAdminTokens.query._type, +) { + const where = { + status: { + in: ['active'] as $Enums.TokenStatus[], + }, + } as const satisfies Prisma.AdminTokenWhereInput; - return prisma.adminToken.findMany({ - omit: { hash: true }, - include: { owner: true }, - orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], - where, - }).then(tokens => - tokens.map(({ permissions, ...token }) => ({ permissions: permissions.toString(), ...token })), - ) + if (query?.withRevoked) where.status.in.push('revoked'); + + return prisma.adminToken + .findMany({ + omit: { hash: true }, + include: { owner: true }, + orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], + where, + }) + .then((tokens) => + tokens.map(({ permissions, ...token }) => ({ + permissions: permissions.toString(), + ...token, + })), + ); } -export async function createToken(data: typeof adminTokenContract.createAdminToken.body._type) { - if (data.expirationDate && !isAtLeastTomorrow(new Date(data.expirationDate))) { - return new BadRequest400('Date d\'expiration trop courte') - } - const password = generateRandomPassword(48, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') - const hash = createHash('sha256').update(password).digest('hex') - const botUserId = randomUUID() - await prisma.user.create({ - data: { - firstName: 'Bot Admin', - lastName: data.name, - type: 'bot', - id: botUserId, - email: `${botUserId}@bot.io`, - }, - }) - const token = await prisma.adminToken.create({ - data: { - ...data, - hash, - permissions: BigInt(data.permissions), - expirationDate: data.expirationDate ? new Date(data.expirationDate) : undefined, - userId: botUserId, - }, - omit: { hash: true }, - include: { owner: true }, - }) - return { - ...token, - password, - permissions: token.permissions.toString(), - } +export async function createToken( + data: typeof adminTokenContract.createAdminToken.body._type, +) { + if ( + data.expirationDate && + !isAtLeastTomorrow(new Date(data.expirationDate)) + ) { + return new BadRequest400("Date d'expiration trop courte"); + } + const password = generateRandomPassword( + 48, + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-', + ); + const hash = createHash('sha256').update(password).digest('hex'); + const botUserId = randomUUID(); + await prisma.user.create({ + data: { + firstName: 'Bot Admin', + lastName: data.name, + type: 'bot', + id: botUserId, + email: `${botUserId}@bot.io`, + }, + }); + const token = await prisma.adminToken.create({ + data: { + ...data, + hash, + permissions: BigInt(data.permissions), + expirationDate: data.expirationDate + ? new Date(data.expirationDate) + : undefined, + userId: botUserId, + }, + omit: { hash: true }, + include: { owner: true }, + }); + return { + ...token, + password, + permissions: token.permissions.toString(), + }; } export async function deleteToken(id: AdminToken['id']) { - return prisma.adminToken.updateMany({ - where: { id }, - data: { - status: 'revoked', - expirationDate: new Date(Date.now()), - }, - }) + return prisma.adminToken.updateMany({ + where: { id }, + data: { + status: 'revoked', + expirationDate: new Date(Date.now()), + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts index 301cf6204..fbb3b2458 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts @@ -1,161 +1,186 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ExposedAdminToken } from '@cpn-console/shared' -import { adminTokenContract } from '@cpn-console/shared' -import type { AdminToken } from '@prisma/client' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getUserMockInfos } from '../../utils/mocks.js' -import { BadRequest400 } from '../../utils/errors.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListTokensMock = vi.spyOn(business, 'listTokens') -const businessCreateTokenMock = vi.spyOn(business, 'createToken') -const businessDeleteTokenMock = vi.spyOn(business, 'deleteToken') +import type { ExposedAdminToken } from '@cpn-console/shared'; +import { adminTokenContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import type { AdminToken } from '@prisma/client'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessListTokensMock = vi.spyOn(business, 'listTokens'); +const businessCreateTokenMock = vi.spyOn(business, 'createToken'); +const businessDeleteTokenMock = vi.spyOn(business, 'deleteToken'); describe('test adminTokenContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listAdminTokens', () => { - it('should return list of admin tokens', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - const tokens: AdminToken[] = [{ - id: faker.string.uuid(), - name: 'token1', - permissions: '2', - lastUse: (new Date()).toISOString(), - expirationDate: null, - status: 'active', - createdAt: (new Date(Date.now())).toISOString(), - }] - businessListTokensMock.mockResolvedValueOnce(tokens) - - const response = await app.inject() - .get(adminTokenContract.listAdminTokens.path) - .end() - - expect(businessListTokensMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(tokens) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(adminTokenContract.listAdminTokens.path) - .end() - - expect(businessListTokensMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('createAdminToken', () => { - it('should create a token for authorized users', async () => { - const user = getUserMockInfos(true) - - const newToken = { - id: faker.string.uuid(), - name: 'test', - lastUse: null, - expirationDate: null, - password: faker.string.alpha({ casing: 'lower', length: 10 }), - permissions: '2', - createdAt: (new Date(Date.now())).toISOString(), - status: 'active', - } - const tokenData: ExposedAdminToken = { - name: newToken.name, - permissions: newToken.permissions, - expirationDate: null, - } - - authUserMock.mockResolvedValueOnce(user) - businessCreateTokenMock.mockResolvedValueOnce(newToken) - - const response = await app.inject() - .post(adminTokenContract.createAdminToken.path) - .body(tokenData) - .end() - - expect(businessCreateTokenMock).toHaveBeenCalledWith(tokenData) - expect(response.json()).toEqual(newToken) - expect(response.statusCode).toEqual(201) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(adminTokenContract.createAdminToken.path) - .body({ - name: 'new-token', - expirationDate: null, - permissions: '4', - }) - .end() - - expect(businessCreateTokenMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - - it('should pass business error', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessCreateTokenMock.mockResolvedValueOnce(new BadRequest400('Invalid date')) - - const response = await app.inject() - .post(adminTokenContract.createAdminToken.path) - .body({ - name: 'new-token', - expirationDate: null, - permissions: '4', - }) - .end() - - expect(businessCreateTokenMock).toHaveBeenCalledTimes(1) - expect(response.statusCode).toEqual(400) - }) - }) - - describe('deleteAdminToken', () => { - const tokenId = faker.string.uuid() - it('should delete a token for authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessDeleteTokenMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .delete(adminTokenContract.deleteAdminToken.path.replace(':tokenId', tokenId)) - .end() - - expect(businessDeleteTokenMock).toHaveBeenCalledWith(tokenId) - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(adminTokenContract.deleteAdminToken.path.replace(':tokenId', tokenId)) - .end() - - expect(businessDeleteTokenMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('listAdminTokens', () => { + it('should return list of admin tokens', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + const tokens: AdminToken[] = [ + { + id: faker.string.uuid(), + name: 'token1', + permissions: '2', + lastUse: new Date().toISOString(), + expirationDate: null, + status: 'active', + createdAt: new Date(Date.now()).toISOString(), + }, + ]; + businessListTokensMock.mockResolvedValueOnce(tokens); + + const response = await app + .inject() + .get(adminTokenContract.listAdminTokens.path) + .end(); + + expect(businessListTokensMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(tokens); + expect(response.statusCode).toEqual(200); + }); + + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(adminTokenContract.listAdminTokens.path) + .end(); + + expect(businessListTokensMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('createAdminToken', () => { + it('should create a token for authorized users', async () => { + const user = getUserMockInfos(true); + + const newToken = { + id: faker.string.uuid(), + name: 'test', + lastUse: null, + expirationDate: null, + password: faker.string.alpha({ casing: 'lower', length: 10 }), + permissions: '2', + createdAt: new Date(Date.now()).toISOString(), + status: 'active', + }; + const tokenData: ExposedAdminToken = { + name: newToken.name, + permissions: newToken.permissions, + expirationDate: null, + }; + + authUserMock.mockResolvedValueOnce(user); + businessCreateTokenMock.mockResolvedValueOnce(newToken); + + const response = await app + .inject() + .post(adminTokenContract.createAdminToken.path) + .body(tokenData) + .end(); + + expect(businessCreateTokenMock).toHaveBeenCalledWith(tokenData); + expect(response.json()).toEqual(newToken); + expect(response.statusCode).toEqual(201); + }); + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(adminTokenContract.createAdminToken.path) + .body({ + name: 'new-token', + expirationDate: null, + permissions: '4', + }) + .end(); + + expect(businessCreateTokenMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + + it('should pass business error', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessCreateTokenMock.mockResolvedValueOnce( + new BadRequest400('Invalid date'), + ); + + const response = await app + .inject() + .post(adminTokenContract.createAdminToken.path) + .body({ + name: 'new-token', + expirationDate: null, + permissions: '4', + }) + .end(); + + expect(businessCreateTokenMock).toHaveBeenCalledTimes(1); + expect(response.statusCode).toEqual(400); + }); + }); + + describe('deleteAdminToken', () => { + const tokenId = faker.string.uuid(); + it('should delete a token for authorized users', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessDeleteTokenMock.mockResolvedValueOnce(null); + + const response = await app + .inject() + .delete( + adminTokenContract.deleteAdminToken.path.replace( + ':tokenId', + tokenId, + ), + ) + .end(); + + expect(businessDeleteTokenMock).toHaveBeenCalledWith(tokenId); + expect(response.statusCode).toEqual(204); + }); + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + adminTokenContract.deleteAdminToken.path.replace( + ':tokenId', + tokenId, + ), + ) + .end(); + + expect(businessDeleteTokenMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts index bd3afb29e..1869769bd 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts @@ -1,44 +1,48 @@ -import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared' -import { serverInstance } from '../../app.js' -import { createToken, deleteToken, listTokens } from './business.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' +import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared'; +import { authUser } from '@old-server/utils/controller.js'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; + +import { serverInstance } from '../../app.js'; +import { createToken, deleteToken, listTokens } from './business.js'; export function adminTokenRouter() { - return serverInstance.router(adminTokenContract, { - listAdminTokens: async ({ request: req, query }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const body = await listTokens(query) - - return { - status: 200, - body, - } - }, - - createAdminToken: async ({ request: req, body: data }) => { - const perms = await authUser(req) - - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const body = await createToken(data) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - deleteAdminToken: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - await deleteToken(params.tokenId) - - return { - status: 204, - body: null, - } - }, - }) + return serverInstance.router(adminTokenContract, { + listAdminTokens: async ({ request: req, query }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await listTokens(query); + + return { + status: 200, + body, + }; + }, + + createAdminToken: async ({ request: req, body: data }) => { + const perms = await authUser(req); + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await createToken(data); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + deleteAdminToken: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + await deleteToken(params.tokenId); + + return { + status: 204, + body: null, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts index 7815132d8..e1115ab87 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts @@ -1,173 +1,293 @@ -import { describe, expect, it, vi } from 'vitest' -import { faker } from '@faker-js/faker' -import type { Cluster, Environment } from '@prisma/client' -import prisma from '../../__mocks__/prisma.js' -import { hook } from '../../__mocks__/utils/hook-wrapper.ts' -import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '../../utils/errors.ts' -import { createCluster, deleteCluster, getClusterAssociatedEnvironments, getClusterDetails, getClusterUsage, listClusters, updateCluster } from './business.ts' +import { faker } from '@faker-js/faker'; +import type { Cluster, Environment } from '@prisma/client'; +import { describe, expect, it, vi } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import { hook } from '../../__mocks__/utils/hook-wrapper.ts'; +import { + BadRequest400, + ErrorResType, + NotFound404, + Unprocessable422, +} from '../../utils/errors.ts'; +import { + createCluster, + deleteCluster, + getClusterAssociatedEnvironments, + getClusterDetails, + getClusterUsage, + listClusters, + updateCluster, +} from './business.ts'; vi.mock('../../utils/hook-wrapper.ts', async () => ({ - hook, -})) + hook, +})); -const userId = faker.string.uuid() -const requestId = faker.string.uuid() +const userId = faker.string.uuid(); +const requestId = faker.string.uuid(); const cluster: Cluster = { - id: faker.string.uuid(), - infos: faker.lorem.lines(2), - privacy: 'public', - createdAt: new Date(), - updatedAt: new Date(), - zoneId: faker.string.uuid(), - clusterResources: false, - kubeConfigId: faker.string.uuid(), - label: faker.string.alpha(10), - secretName: faker.string.alpha(10), - external: false, - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), -} + id: faker.string.uuid(), + infos: faker.lorem.lines(2), + privacy: 'public', + createdAt: new Date(), + updatedAt: new Date(), + zoneId: faker.string.uuid(), + clusterResources: false, + kubeConfigId: faker.string.uuid(), + label: faker.string.alpha(10), + secretName: faker.string.alpha(10), + external: false, + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), +}; describe('test Cluster business logic', () => { - describe('listClusters', () => { - it('should filter for user', async () => { - prisma.cluster.findMany.mockResolvedValue([]) - await listClusters(userId) - expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1) - expect(prisma.cluster.findMany).toHaveBeenCalledWith({ select: expect.any(Object), where: { OR: [{ privacy: 'public' }, expect.any(Object), expect.any(Object), expect.any(Object)] } }) - }) - it('should not filter', async () => { - const dbStages = [{ id: faker.string.uuid() }] - prisma.cluster.findMany.mockResolvedValue([{ stages: dbStages }] as unknown as Cluster[]) - const response = await listClusters() - expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1) - expect(prisma.cluster.findMany).toHaveBeenCalledWith({ select: expect.any(Object), where: {} }) - expect(response[0].stageIds).toStrictEqual([dbStages[0].id]) - }) - }) - - describe('getClusterAssociatedEnvironments', () => { - it('should list all environments attached to a cluster', async () => { - const envName = faker.string.alpha(8) - const projectName = faker.string.alpha(8) - const ownerEmail = faker.internet.email() - const cpu = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) - const gpu = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) - const memory = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) - const envs = [{ name: envName, cpu, gpu, memory, project: { name: projectName, owner: { email: ownerEmail } } }] as unknown as Environment[] - prisma.environment.findMany.mockResolvedValue(envs) - const response = await getClusterAssociatedEnvironments(cluster.id) - expect(response).toStrictEqual([{ - name: envName, - project: projectName, - owner: ownerEmail, - cpu, - gpu, - memory, - }]) - }) - }) - - describe('getClusterDetails', () => { - it('should return a cluster details', async () => { - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) - await getClusterDetails(cluster.id) - }) - it('should return a cluster details, without infos in db', async () => { - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, infos: null, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) - const response = await getClusterDetails(cluster.id) - expect(response.infos).toBe('') - }) - }) - - describe('getClusterUsage', () => { - it('should return a cluster usage', async () => { - prisma.environment.aggregate.mockResolvedValue({ _count: {}, _avg: {}, _min: {}, _max: {}, _sum: { - cpu: 10, - gpu: 5, - memory: 20, - } }) - const response = await getClusterUsage(cluster.id) - expect(response).toStrictEqual({ - cpu: 10, - gpu: 5, - memory: 20, - }) - }) - }) - - describe('createCluster', () => { - it('should create cluster', async () => { - hook.cluster.upsert.mockResolvedValue({ failed: false }) - prisma.cluster.findUnique.mockResolvedValue(null) - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) - prisma.cluster.create.mockResolvedValue(cluster) - - const response = await createCluster({ - infos: faker.string.alpha(10), - zoneId: faker.string.uuid(), - privacy: 'public', - stageIds: [], - clusterResources: false, - kubeconfig: { cluster: { tlsServerName: faker.internet.domainName() }, user: {} }, - label: faker.string.alpha(10), - external: false, - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - }, userId, requestId) - - expect(response).not.instanceOf(ErrorResType) - expect(prisma.cluster.create).toHaveBeenCalled() - }) - }) - - describe('updateCluster', () => { - it('should update cluster', async () => { - hook.cluster.upsert.mockResolvedValue({ failed: false }) - prisma.cluster.findUnique.mockResolvedValue(cluster) - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) - prisma.cluster.update.mockResolvedValue(cluster) - - const response = await updateCluster({ - infos: faker.string.alpha(10), - zoneId: faker.string.uuid(), - privacy: 'public', - stageIds: [], - }, cluster.id, userId, requestId) - - expect(response).not.instanceOf(ErrorResType) - expect(prisma.cluster.update).toHaveBeenCalled() - }) - it('should return 404', async () => { - prisma.cluster.findUnique.mockResolvedValue(null) - const response = await updateCluster({ infos: faker.string.alpha(10) }, cluster.id, userId, requestId) - expect(response).instanceOf(NotFound404) - }) - }) - - describe('deleteCluster', () => { - it('should delete cluster', async () => { - hook.cluster.delete.mockResolvedValue({}) - await deleteCluster({ clusterId: cluster.id, userId, requestId }) - - expect(prisma.cluster.delete).toHaveBeenCalledTimes(1) - }) - it('should return failed hook', async () => { - hook.cluster.delete.mockResolvedValue({ failed: true }) - const response = await deleteCluster({ clusterId: cluster.id, userId, requestId }) - - expect(response).instanceOf(Unprocessable422) - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) - }) - it('should not delete cluster, env attached', async () => { - prisma.environment.findFirst.mockResolvedValue({ id: faker.string.uuid() } as Environment) - const response = await deleteCluster({ clusterId: cluster.id, userId, requestId }) - - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) - expect(response).instanceOf(BadRequest400) - }) - }) -}) + describe('listClusters', () => { + it('should filter for user', async () => { + prisma.cluster.findMany.mockResolvedValue([]); + await listClusters(userId); + expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1); + expect(prisma.cluster.findMany).toHaveBeenCalledWith({ + select: expect.any(Object), + where: { + OR: [ + { privacy: 'public' }, + expect.any(Object), + expect.any(Object), + expect.any(Object), + ], + }, + }); + }); + it('should not filter', async () => { + const dbStages = [{ id: faker.string.uuid() }]; + prisma.cluster.findMany.mockResolvedValue([ + { stages: dbStages }, + ] as unknown as Cluster[]); + const response = await listClusters(); + expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1); + expect(prisma.cluster.findMany).toHaveBeenCalledWith({ + select: expect.any(Object), + where: {}, + }); + expect(response[0].stageIds).toStrictEqual([dbStages[0].id]); + }); + }); + + describe('getClusterAssociatedEnvironments', () => { + it('should list all environments attached to a cluster', async () => { + const envName = faker.string.alpha(8); + const projectName = faker.string.alpha(8); + const ownerEmail = faker.internet.email(); + const cpu = faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }); + const gpu = faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }); + const memory = faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }); + const envs = [ + { + name: envName, + cpu, + gpu, + memory, + project: { + name: projectName, + owner: { email: ownerEmail }, + }, + }, + ] as unknown as Environment[]; + prisma.environment.findMany.mockResolvedValue(envs); + const response = await getClusterAssociatedEnvironments(cluster.id); + expect(response).toStrictEqual([ + { + name: envName, + project: projectName, + owner: ownerEmail, + cpu, + gpu, + memory, + }, + ]); + }); + }); + + describe('getClusterDetails', () => { + it('should return a cluster details', async () => { + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ + ...cluster, + projects: [], + stages: [], + kubeconfig: { user: {}, cluster: {} }, + } as Cluster); + await getClusterDetails(cluster.id); + }); + it('should return a cluster details, without infos in db', async () => { + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ + ...cluster, + infos: null, + projects: [], + stages: [], + kubeconfig: { user: {}, cluster: {} }, + } as Cluster); + const response = await getClusterDetails(cluster.id); + expect(response.infos).toBe(''); + }); + }); + + describe('getClusterUsage', () => { + it('should return a cluster usage', async () => { + prisma.environment.aggregate.mockResolvedValue({ + _count: {}, + _avg: {}, + _min: {}, + _max: {}, + _sum: { + cpu: 10, + gpu: 5, + memory: 20, + }, + }); + const response = await getClusterUsage(cluster.id); + expect(response).toStrictEqual({ + cpu: 10, + gpu: 5, + memory: 20, + }); + }); + }); + + describe('createCluster', () => { + it('should create cluster', async () => { + hook.cluster.upsert.mockResolvedValue({ failed: false }); + prisma.cluster.findUnique.mockResolvedValue(null); + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ + ...cluster, + projects: [], + stages: [], + kubeconfig: { user: {}, cluster: {} }, + } as Cluster); + prisma.cluster.create.mockResolvedValue(cluster); + + const response = await createCluster( + { + infos: faker.string.alpha(10), + zoneId: faker.string.uuid(), + privacy: 'public', + stageIds: [], + clusterResources: false, + kubeconfig: { + cluster: { tlsServerName: faker.internet.domainName() }, + user: {}, + }, + label: faker.string.alpha(10), + external: false, + cpu: faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }), + gpu: faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }), + memory: faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }), + }, + userId, + requestId, + ); + + expect(response).not.instanceOf(ErrorResType); + expect(prisma.cluster.create).toHaveBeenCalled(); + }); + }); + + describe('updateCluster', () => { + it('should update cluster', async () => { + hook.cluster.upsert.mockResolvedValue({ failed: false }); + prisma.cluster.findUnique.mockResolvedValue(cluster); + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ + ...cluster, + projects: [], + stages: [], + kubeconfig: { user: {}, cluster: {} }, + } as Cluster); + prisma.cluster.update.mockResolvedValue(cluster); + + const response = await updateCluster( + { + infos: faker.string.alpha(10), + zoneId: faker.string.uuid(), + privacy: 'public', + stageIds: [], + }, + cluster.id, + userId, + requestId, + ); + + expect(response).not.instanceOf(ErrorResType); + expect(prisma.cluster.update).toHaveBeenCalled(); + }); + it('should return 404', async () => { + prisma.cluster.findUnique.mockResolvedValue(null); + const response = await updateCluster( + { infos: faker.string.alpha(10) }, + cluster.id, + userId, + requestId, + ); + expect(response).instanceOf(NotFound404); + }); + }); + + describe('deleteCluster', () => { + it('should delete cluster', async () => { + hook.cluster.delete.mockResolvedValue({}); + await deleteCluster({ clusterId: cluster.id, userId, requestId }); + + expect(prisma.cluster.delete).toHaveBeenCalledTimes(1); + }); + it('should return failed hook', async () => { + hook.cluster.delete.mockResolvedValue({ failed: true }); + const response = await deleteCluster({ + clusterId: cluster.id, + userId, + requestId, + }); + + expect(response).instanceOf(Unprocessable422); + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0); + }); + it('should not delete cluster, env attached', async () => { + prisma.environment.findFirst.mockResolvedValue({ + id: faker.string.uuid(), + } as Environment); + const response = await deleteCluster({ + clusterId: cluster.id, + userId, + requestId, + }); + + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0); + expect(response).instanceOf(BadRequest400); + }); + }); +}); // findUniqueOrThrow diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts index 04a0e6858..990726b21 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts @@ -1,230 +1,287 @@ -import type { Prisma, Project, User } from '@prisma/client' -import type { Cluster, ClusterDetails, Kubeconfig, clusterContract } from '@cpn-console/shared' -import { ClusterDetailsSchema, ClusterPrivacy } from '@cpn-console/shared' +import type { + Cluster, + ClusterDetails, + Kubeconfig, + clusterContract, +} from '@cpn-console/shared'; +import { ClusterDetailsSchema, ClusterPrivacy } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; import { - addLogs, - createCluster as createClusterQuery, - deleteCluster as deleteClusterQuery, - getClusterById, - getClusterByLabel, - getClusterDetails as getClusterDetailsQuery, - getClusterEnvironments, - getProjectsByClusterId, - linkClusterToProjects, - linkZoneToClusters, - listClusters as listClustersQuery, - listStagesByClusterId, - removeClusterFromProject, - removeClusterFromStage, - updateCluster as updateClusterQuery, -} from '@old-server/resources/queries-index.js' -import { linkClusterToStages } from '@old-server/resources/stage/business.js' -import { validateSchema } from '@old-server/utils/business.js' -import { hook } from '@old-server/utils/hook-wrapper.js' -import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '@old-server/utils/errors.js' -import prisma from '@old-server/prisma.js' -import type { Resources } from '@old-server/types/index.js' + addLogs, + createCluster as createClusterQuery, + deleteCluster as deleteClusterQuery, + getClusterById, + getClusterByLabel, + getClusterDetails as getClusterDetailsQuery, + getClusterEnvironments, + getProjectsByClusterId, + linkClusterToProjects, + linkZoneToClusters, + listClusters as listClustersQuery, + listStagesByClusterId, + removeClusterFromProject, + removeClusterFromStage, + updateCluster as updateClusterQuery, +} from '@old-server/resources/queries-index.js'; +import { linkClusterToStages } from '@old-server/resources/stage/business.js'; +import type { Resources } from '@old-server/types/index.js'; +import { validateSchema } from '@old-server/utils/business.js'; +import { + BadRequest400, + ErrorResType, + NotFound404, + Unprocessable422, +} from '@old-server/utils/errors.js'; +import { hook } from '@old-server/utils/hook-wrapper.js'; +import type { Prisma, Project, User } from '@prisma/client'; export async function listClusters(userId?: User['id']) { - const where: Prisma.ClusterWhereInput = userId - ? { - OR: [ - // Sélectionne tous les clusters publics - { privacy: 'public' }, - // Sélectionne les clusters associés aux projets dont l'user est membre - { - projects: { some: { members: { some: { userId } } } }, - }, - // Sélectionne les clusters associés aux projets dont l'user est owner - { - projects: { some: { ownerId: userId } }, - }, - // Sélectionne les clusters associés aux environnments appartenant à des projets dont l'user est membre - { - environments: { some: { project: { members: { some: { userId } } } } }, - }, - ], - } - : {} - const clusters = await listClustersQuery(where) - return clusters.map(({ stages, ...cluster }) => ({ - ...cluster, - stageIds: stages.map(({ id }) => id), - })) + const where: Prisma.ClusterWhereInput = userId + ? { + OR: [ + // Sélectionne tous les clusters publics + { privacy: 'public' }, + // Sélectionne les clusters associés aux projets dont l'user est membre + { + projects: { some: { members: { some: { userId } } } }, + }, + // Sélectionne les clusters associés aux projets dont l'user est owner + { + projects: { some: { ownerId: userId } }, + }, + // Sélectionne les clusters associés aux environnments appartenant à des projets dont l'user est membre + { + environments: { + some: { project: { members: { some: { userId } } } }, + }, + }, + ], + } + : {}; + const clusters = await listClustersQuery(where); + return clusters.map(({ stages, ...cluster }) => ({ + ...cluster, + stageIds: stages.map(({ id }) => id), + })); } export async function getClusterAssociatedEnvironments(clusterId: string) { - const clusterEnvironments = await getClusterEnvironments(clusterId) - - return clusterEnvironments.map((environment) => { - return ({ - project: environment.project?.name, - name: environment.name, - owner: environment.project.owner.email, - cpu: environment.cpu, - gpu: environment.gpu, - memory: environment.memory, - }) - }) + const clusterEnvironments = await getClusterEnvironments(clusterId); + + return clusterEnvironments.map((environment) => { + return { + project: environment.project?.name, + name: environment.name, + owner: environment.project.owner.email, + cpu: environment.cpu, + gpu: environment.gpu, + memory: environment.memory, + }; + }); } -export async function getClusterDetails(clusterId: string): Promise { - const { infos, projects, stages, kubeconfig, ...details } = await getClusterDetailsQuery(clusterId) - return { - ...details, - infos: infos ?? '', - projectIds: projects.map(project => project.id), - stageIds: stages.map(({ id }) => id), - kubeconfig: { - cluster: kubeconfig.cluster as unknown as Kubeconfig['cluster'], - user: kubeconfig.user as unknown as Kubeconfig['user'], - }, - } +export async function getClusterDetails( + clusterId: string, +): Promise { + const { infos, projects, stages, kubeconfig, ...details } = + await getClusterDetailsQuery(clusterId); + return { + ...details, + infos: infos ?? '', + projectIds: projects.map((project) => project.id), + stageIds: stages.map(({ id }) => id), + kubeconfig: { + cluster: kubeconfig.cluster as unknown as Kubeconfig['cluster'], + user: kubeconfig.user as unknown as Kubeconfig['user'], + }, + }; } export async function getClusterUsage(clusterId: string): Promise { - const clusterUsage = await prisma.environment.aggregate({ - _sum: { - memory: true, - cpu: true, - gpu: true, - }, - where: { - clusterId, - }, - }) - return { - cpu: clusterUsage._sum.cpu ?? 0, - gpu: clusterUsage._sum.gpu ?? 0, - memory: clusterUsage._sum.memory ?? 0, - } + const clusterUsage = await prisma.environment.aggregate({ + _sum: { + memory: true, + cpu: true, + gpu: true, + }, + where: { + clusterId, + }, + }); + return { + cpu: clusterUsage._sum.cpu ?? 0, + gpu: clusterUsage._sum.gpu ?? 0, + memory: clusterUsage._sum.memory ?? 0, + }; } -export async function createCluster(data: typeof clusterContract.createCluster.body._type, userId: User['id'], requestId: string) { - const isLabelTaken = await getClusterByLabel(data.label) - if (isLabelTaken) return new BadRequest400('Ce label existe déjà pour un autre cluster') +export async function createCluster( + data: typeof clusterContract.createCluster.body._type, + userId: User['id'], + requestId: string, +) { + const isLabelTaken = await getClusterByLabel(data.label); + if (isLabelTaken) + return new BadRequest400('Ce label existe déjà pour un autre cluster'); - data.projectIds = data.privacy === ClusterPrivacy.PUBLIC - ? [] - : data.projectIds ?? [] + data.projectIds = + data.privacy === ClusterPrivacy.PUBLIC ? [] : (data.projectIds ?? []); - const { - projectIds, - stageIds, - kubeconfig, - zoneId, - ...clusterData - } = data + const { projectIds, stageIds, kubeconfig, zoneId, ...clusterData } = data; - const clusterCreated = await createClusterQuery(clusterData, kubeconfig, zoneId) + const clusterCreated = await createClusterQuery( + clusterData, + kubeconfig, + zoneId, + ); - if (data.privacy === ClusterPrivacy.DEDICATED && projectIds.length) { - await linkClusterToProjects(clusterCreated.id, projectIds) - } + if (data.privacy === ClusterPrivacy.DEDICATED && projectIds.length) { + await linkClusterToProjects(clusterCreated.id, projectIds); + } - if (stageIds?.length) { - await linkClusterToStages(clusterCreated.id, stageIds) - } + if (stageIds?.length) { + await linkClusterToStages(clusterCreated.id, stageIds); + } - const hookReply = await hook.cluster.upsert(clusterCreated.id, zoneId) - await addLogs({ action: 'Create Cluster', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la création du cluster') - } + const hookReply = await hook.cluster.upsert(clusterCreated.id, zoneId); + await addLogs({ + action: 'Create Cluster', + data: hookReply, + userId, + requestId, + }); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services à la création du cluster', + ); + } - return getClusterDetails(clusterCreated.id) + return getClusterDetails(clusterCreated.id); } -export async function updateCluster(data: typeof clusterContract.updateCluster.body._type, clusterId: Cluster['id'], userId: User['id'], requestId: string): Promise { - if (data?.privacy === ClusterPrivacy.PUBLIC) delete data.projectIds - - const schemaValidation = ClusterDetailsSchema.partial().safeParse({ ...data, id: clusterId }) - const validateResult = validateSchema(schemaValidation) - if (validateResult instanceof ErrorResType) return validateResult - - const dbCluster = await getClusterById(clusterId) - if (!dbCluster) return new NotFound404() - - const { - projectIds, - stageIds, - kubeconfig, - zoneId, - ...clusterData - } = data - - const clusterUpdated = await updateClusterQuery(clusterId, clusterData, - // @ts-ignore - kubeconfig) - - // zone - if (zoneId) { - await linkZoneToClusters(zoneId, [clusterId]) - } - - // projects - const dbProjects = await getProjectsByClusterId(clusterId) - - let projectsToRemove: Project['id'][] = [] - - if (projectIds && clusterUpdated.privacy === ClusterPrivacy.DEDICATED) { - await linkClusterToProjects(clusterId, projectIds) - projectsToRemove = dbProjects?.map(project => project.id)?.filter(dbProjectId => !projectIds.includes(dbProjectId)) ?? [] - } else if (clusterUpdated.privacy === ClusterPrivacy.PUBLIC) { - projectsToRemove = dbProjects?.map(project => project.id) ?? [] - } - - for (const projectId of projectsToRemove) { - await removeClusterFromProject(clusterUpdated.id, projectId) - } - - // stages - if (stageIds) { - await linkClusterToStages(clusterId, stageIds) - - const dbStages = await listStagesByClusterId(clusterId) - if (dbStages) { - for (const stage of dbStages) { - if (!stageIds.includes(stage.id)) { - await removeClusterFromStage(clusterUpdated.id, stage.id) +export async function updateCluster( + data: typeof clusterContract.updateCluster.body._type, + clusterId: Cluster['id'], + userId: User['id'], + requestId: string, +): Promise { + if (data?.privacy === ClusterPrivacy.PUBLIC) delete data.projectIds; + + const schemaValidation = ClusterDetailsSchema.partial().safeParse({ + ...data, + id: clusterId, + }); + const validateResult = validateSchema(schemaValidation); + if (validateResult instanceof ErrorResType) return validateResult; + + const dbCluster = await getClusterById(clusterId); + if (!dbCluster) return new NotFound404(); + + const { projectIds, stageIds, kubeconfig, zoneId, ...clusterData } = data; + + const clusterUpdated = await updateClusterQuery( + clusterId, + clusterData, + // @ts-ignore + kubeconfig, + ); + + // zone + if (zoneId) { + await linkZoneToClusters(zoneId, [clusterId]); + } + + // projects + const dbProjects = await getProjectsByClusterId(clusterId); + + let projectsToRemove: Project['id'][] = []; + + if (projectIds && clusterUpdated.privacy === ClusterPrivacy.DEDICATED) { + await linkClusterToProjects(clusterId, projectIds); + projectsToRemove = + dbProjects + ?.map((project) => project.id) + ?.filter((dbProjectId) => !projectIds.includes(dbProjectId)) ?? + []; + } else if (clusterUpdated.privacy === ClusterPrivacy.PUBLIC) { + projectsToRemove = dbProjects?.map((project) => project.id) ?? []; + } + + for (const projectId of projectsToRemove) { + await removeClusterFromProject(clusterUpdated.id, projectId); + } + + // stages + if (stageIds) { + await linkClusterToStages(clusterId, stageIds); + + const dbStages = await listStagesByClusterId(clusterId); + if (dbStages) { + for (const stage of dbStages) { + if (!stageIds.includes(stage.id)) { + await removeClusterFromStage(clusterUpdated.id, stage.id); + } + } } - } } - } - const hookReply = await hook.cluster.upsert(clusterId, dbCluster.zoneId) - await addLogs({ action: 'Update Cluster', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la mise à jour du cluster') - } + const hookReply = await hook.cluster.upsert(clusterId, dbCluster.zoneId); + await addLogs({ + action: 'Update Cluster', + data: hookReply, + userId, + requestId, + }); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services à la mise à jour du cluster', + ); + } - return getClusterDetails(clusterId) + return getClusterDetails(clusterId); } interface DeleteClusterArgs { - clusterId: Cluster['id'] - userId?: User['id'] - requestId: string - force?: boolean + clusterId: Cluster['id']; + userId?: User['id']; + requestId: string; + force?: boolean; } -export async function deleteCluster({ clusterId, requestId, force, userId }: DeleteClusterArgs) { - let message: string | null = null - if (force) { - const envs = await prisma.environment.deleteMany({ - where: { clusterId }, - }) - message = `${envs.count} environnements supprimés de force, n'oubliez pas de reprovisionner les projets concernés` - } else { - const environment = await prisma.environment.findFirst({ where: { clusterId } }) - if (environment) return new BadRequest400('Impossible de supprimer le cluster, des environnements en activité y sont déployés') - } - - const hookReply = await hook.cluster.delete(clusterId) - await addLogs({ action: 'Delete Cluster', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la suppression du cluster') - } - - await deleteClusterQuery(clusterId) - return message +export async function deleteCluster({ + clusterId, + requestId, + force, + userId, +}: DeleteClusterArgs) { + let message: string | null = null; + if (force) { + const envs = await prisma.environment.deleteMany({ + where: { clusterId }, + }); + message = `${envs.count} environnements supprimés de force, n'oubliez pas de reprovisionner les projets concernés`; + } else { + const environment = await prisma.environment.findFirst({ + where: { clusterId }, + }); + if (environment) + return new BadRequest400( + 'Impossible de supprimer le cluster, des environnements en activité y sont déployés', + ); + } + + const hookReply = await hook.cluster.delete(clusterId); + await addLogs({ + action: 'Delete Cluster', + data: hookReply, + userId, + requestId, + }); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services à la suppression du cluster', + ); + } + + await deleteClusterQuery(clusterId); + return message; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts index b49190161..b3777acc0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts @@ -1,312 +1,361 @@ -import type { Cluster, Environment, Kubeconfig, Prisma, Project, Stage } from '@prisma/client' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { + Cluster, + Environment, + Kubeconfig, + Prisma, + Project, + Stage, +} from '@prisma/client'; -export async function getClustersAssociatedWithProject(projectId: Project['id']) { - const [ - clusterIdsHistory, - clusterIdsEnv, - ] = await Promise.all([ - prisma.projectClusterHistory.findMany({ - select: { - clusterId: true, - }, - where: { - projectId, - }, - }).then(history => history.map(({ clusterId }) => clusterId)), - prisma.cluster.findMany({ - where: { environments: { some: { project: { id: projectId } } } }, - select: { id: true }, - }).then(cluster => cluster.map(({ id }) => id)), - ]) - const clusterIds = [ - ...clusterIdsHistory, - ...clusterIdsEnv.filter(id => !clusterIdsHistory.includes(id)), - ] - return prisma.cluster.findMany({ - where: { id: { in: clusterIds } }, - select: { - id: true, - infos: true, - label: true, - external: true, - privacy: true, - secretName: true, - kubeconfig: true, - clusterResources: true, - cpu: true, - gpu: true, - memory: true, - zone: { +export async function getClustersAssociatedWithProject( + projectId: Project['id'], +) { + const [clusterIdsHistory, clusterIdsEnv] = await Promise.all([ + prisma.projectClusterHistory + .findMany({ + select: { + clusterId: true, + }, + where: { + projectId, + }, + }) + .then((history) => history.map(({ clusterId }) => clusterId)), + prisma.cluster + .findMany({ + where: { + environments: { some: { project: { id: projectId } } }, + }, + select: { id: true }, + }) + .then((cluster) => cluster.map(({ id }) => id)), + ]); + const clusterIds = [ + ...clusterIdsHistory, + ...clusterIdsEnv.filter((id) => !clusterIdsHistory.includes(id)), + ]; + return prisma.cluster.findMany({ + where: { id: { in: clusterIds } }, select: { - id: true, - slug: true, - argocdUrl: true, - label: true, + id: true, + infos: true, + label: true, + external: true, + privacy: true, + secretName: true, + kubeconfig: true, + clusterResources: true, + cpu: true, + gpu: true, + memory: true, + zone: { + select: { + id: true, + slug: true, + argocdUrl: true, + label: true, + }, + }, }, - }, - }, - }) + }); } -export async function updateProjectClusterHistory(projectId: Project['id'], clusterIds: Cluster['id'][]) { - return prisma.$transaction([ - prisma.projectClusterHistory.deleteMany({ - where: { - AND: { - projectId, - clusterId: { notIn: clusterIds }, - }, - }, - }), - prisma.projectClusterHistory.createMany({ - data: clusterIds.map(clusterId => ({ clusterId, projectId })), - skipDuplicates: true, - }), - ]) +export async function updateProjectClusterHistory( + projectId: Project['id'], + clusterIds: Cluster['id'][], +) { + return prisma.$transaction([ + prisma.projectClusterHistory.deleteMany({ + where: { + AND: { + projectId, + clusterId: { notIn: clusterIds }, + }, + }, + }), + prisma.projectClusterHistory.createMany({ + data: clusterIds.map((clusterId) => ({ clusterId, projectId })), + skipDuplicates: true, + }), + ]); } export function getClusterById(id: Cluster['id']) { - return prisma.cluster.findUnique({ - where: { id }, - include: { kubeconfig: true }, - }) + return prisma.cluster.findUnique({ + where: { id }, + include: { kubeconfig: true }, + }); } export function getClusterByIdOrThrow(id: Cluster['id']) { - return prisma.cluster.findUniqueOrThrow({ - where: { id }, - include: { kubeconfig: true, zone: true }, - }) + return prisma.cluster.findUniqueOrThrow({ + where: { id }, + include: { kubeconfig: true, zone: true }, + }); } export function getClusterEnvironments(clusterId: Cluster['id']) { - return prisma.environment.findMany({ - where: { clusterId }, - select: { - name: true, - cpu: true, - gpu: true, - memory: true, - project: { + return prisma.environment.findMany({ + where: { clusterId }, select: { - slug: true, - name: true, - owner: true, - members: true, + name: true, + cpu: true, + gpu: true, + memory: true, + project: { + select: { + slug: true, + name: true, + owner: true, + members: true, + }, + }, }, - }, - }, - }) + }); } export function getClusterDetails(id: Cluster['id']) { - return prisma.cluster.findUniqueOrThrow({ - where: { id }, - select: { - createdAt: true, - projects: { + return prisma.cluster.findUniqueOrThrow({ + where: { id }, select: { - id: true, + createdAt: true, + projects: { + select: { + id: true, + }, + }, + id: true, + clusterResources: true, + infos: true, + external: true, + label: true, + privacy: true, + kubeconfig: true, + stages: true, + updatedAt: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, }, - }, - id: true, - clusterResources: true, - infos: true, - external: true, - label: true, - privacy: true, - kubeconfig: true, - stages: true, - updatedAt: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, - }, - }) + }); } export function getClustersByIds(clusterIds: Cluster['id'][]) { - return prisma.cluster.findMany({ - where: { - id: { in: clusterIds }, - }, - include: { kubeconfig: true }, - }) + return prisma.cluster.findMany({ + where: { + id: { in: clusterIds }, + }, + include: { kubeconfig: true }, + }); } export function getPublicClusters() { - return prisma.cluster.findMany({ - where: { privacy: 'public' }, - include: { zone: true }, - }) + return prisma.cluster.findMany({ + where: { privacy: 'public' }, + include: { zone: true }, + }); } export async function getClusterNamesByZoneId(zoneId: string) { - const clusterNames = await prisma.cluster.findMany({ - where: { zoneId }, - select: { - label: true, - }, - }) - return clusterNames.map(({ label }) => label) + const clusterNames = await prisma.cluster.findMany({ + where: { zoneId }, + select: { + label: true, + }, + }); + return clusterNames.map(({ label }) => label); } export function getClusterByLabel(label: Cluster['label']) { - return prisma.cluster.findUnique({ where: { label } }) + return prisma.cluster.findUnique({ where: { label } }); } export function getClusterByEnvironmentId(id: Environment['id']) { - return prisma.cluster.findMany({ - where: { - environments: { - some: { id }, - }, - }, - include: { kubeconfig: true }, - }) + return prisma.cluster.findMany({ + where: { + environments: { + some: { id }, + }, + }, + include: { kubeconfig: true }, + }); } export function getClustersWithProjectIdAndConfig() { - return prisma.cluster.findMany({ - select: { - id: true, - stages: true, - projects: { - where: { - status: { not: 'archived' }, - }, + return prisma.cluster.findMany({ select: { - id: true, - name: true, - slug: true, - status: true, + id: true, + stages: true, + projects: { + where: { + status: { not: 'archived' }, + }, + select: { + id: true, + name: true, + slug: true, + status: true, + }, + }, + clusterResources: true, + label: true, + infos: true, + privacy: true, + secretName: true, + kubeconfig: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, }, - }, - clusterResources: true, - label: true, - infos: true, - privacy: true, - secretName: true, - kubeconfig: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, - }, - }) + }); } export function listClusters(where: Prisma.ClusterWhereInput) { - return prisma.cluster.findMany({ - where, - select: { - id: true, - label: true, - stages: true, - clusterResources: true, - privacy: true, - infos: true, - external: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, - }, - }) + return prisma.cluster.findMany({ + where, + select: { + id: true, + label: true, + stages: true, + clusterResources: true, + privacy: true, + infos: true, + external: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, + }, + }); } export async function getProjectsByClusterId(id: Cluster['id']) { - return (await prisma.cluster.findUniqueOrThrow({ - where: { id }, - select: { projects: true }, - }))?.projects + return ( + await prisma.cluster.findUniqueOrThrow({ + where: { id }, + select: { projects: true }, + }) + )?.projects; } export async function listStagesByClusterId(id: Cluster['id']) { - return (await prisma.cluster.findUniqueOrThrow({ - where: { id }, - select: { stages: true }, - }))?.stages + return ( + await prisma.cluster.findUniqueOrThrow({ + where: { id }, + select: { stages: true }, + }) + )?.stages; } -export function createCluster(data: Omit, kubeconfig: Pick, zoneId: string) { - return prisma.cluster.create({ - data: { - ...data, - // @ts-ignore - kubeconfig: { create: kubeconfig }, - zone: { - connect: { id: zoneId }, - }, - }, - }) +export function createCluster( + data: Omit< + Cluster, + | 'id' + | 'updatedAt' + | 'createdAt' + | 'kubeConfigId' + | 'secretName' + | 'zoneId' + >, + kubeconfig: Pick, + zoneId: string, +) { + return prisma.cluster.create({ + data: { + ...data, + // @ts-ignore + kubeconfig: { create: kubeconfig }, + zone: { + connect: { id: zoneId }, + }, + }, + }); } -export function updateCluster(id: Cluster['id'], data: Partial>, kubeconfig: Pick) { - return prisma.cluster.update({ - where: { id }, - data: { - ...data, - kubeconfig: { - // @ts-ignore - update: kubeconfig, - }, - }, - }) +export function updateCluster( + id: Cluster['id'], + data: Partial< + Omit + >, + kubeconfig: Pick, +) { + return prisma.cluster.update({ + where: { id }, + data: { + ...data, + kubeconfig: { + // @ts-ignore + update: kubeconfig, + }, + }, + }); } -export function linkClusterToProjects(id: Cluster['id'], projectIds: Project['id'][]) { - return prisma.cluster.update({ - where: { id }, - data: { - projects: { - connect: projectIds.map(projectId => ({ id: projectId })), - }, - }, - }) +export function linkClusterToProjects( + id: Cluster['id'], + projectIds: Project['id'][], +) { + return prisma.cluster.update({ + where: { id }, + data: { + projects: { + connect: projectIds.map((projectId) => ({ id: projectId })), + }, + }, + }); } -export function linkClusterToStages(id: Cluster['id'], stageIds: Stage['id'][]) { - return prisma.cluster.update({ - where: { id }, - data: { - stages: { - connect: stageIds.map(stageId => ({ id: stageId })), - }, - }, - }) +export function linkClusterToStages( + id: Cluster['id'], + stageIds: Stage['id'][], +) { + return prisma.cluster.update({ + where: { id }, + data: { + stages: { + connect: stageIds.map((stageId) => ({ id: stageId })), + }, + }, + }); } -export function removeClusterFromProject(id: Cluster['id'], projectId: Project['id']) { - return prisma.cluster.update({ - where: { id }, - data: { - projects: { - disconnect: { - id: projectId, +export function removeClusterFromProject( + id: Cluster['id'], + projectId: Project['id'], +) { + return prisma.cluster.update({ + where: { id }, + data: { + projects: { + disconnect: { + id: projectId, + }, + }, }, - }, - }, - }) + }); } -export function removeClusterFromStage(id: Cluster['id'], stageId: Stage['id']) { - return prisma.cluster.update({ - where: { id }, - data: { - stages: { - disconnect: { - id: stageId, +export function removeClusterFromStage( + id: Cluster['id'], + stageId: Stage['id'], +) { + return prisma.cluster.update({ + where: { id }, + data: { + stages: { + disconnect: { + id: stageId, + }, + }, }, - }, - }, - }) + }); } export function deleteCluster(id: Cluster['id']) { - return prisma.cluster.delete({ - where: { id }, - }) + return prisma.cluster.delete({ + where: { id }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts index b133835e3..d96e96f06 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts @@ -1,311 +1,412 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ClusterDetails, Environment } from '@cpn-console/shared' -import { clusterContract } from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getUserMockInfos } from '../../utils/mocks.js' -import { BadRequest400 } from '../../utils/errors.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMock = vi.spyOn(business, 'listClusters') -const businessGetDetailsMock = vi.spyOn(business, 'getClusterDetails') -const businessGetUsageMock = vi.spyOn(business, 'getClusterUsage') -const businessGetEnvironmentsMock = vi.spyOn(business, 'getClusterAssociatedEnvironments') -const businessCreateMock = vi.spyOn(business, 'createCluster') -const businessUpdateMock = vi.spyOn(business, 'updateCluster') -const businessDeleteMock = vi.spyOn(business, 'deleteCluster') +import type { ClusterDetails, Environment } from '@cpn-console/shared'; +import { clusterContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessListMock = vi.spyOn(business, 'listClusters'); +const businessGetDetailsMock = vi.spyOn(business, 'getClusterDetails'); +const businessGetUsageMock = vi.spyOn(business, 'getClusterUsage'); +const businessGetEnvironmentsMock = vi.spyOn( + business, + 'getClusterAssociatedEnvironments', +); +const businessCreateMock = vi.spyOn(business, 'createCluster'); +const businessUpdateMock = vi.spyOn(business, 'updateCluster'); +const businessDeleteMock = vi.spyOn(business, 'deleteCluster'); describe('test clusterContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - describe('listClusters', () => { - it('as non admin', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - businessListMock.mockResolvedValueOnce([]) - const response = await app.inject() - .get(clusterContract.listClusters.path) - .end() - - expect(businessListMock).toHaveBeenCalledWith(user.user.id) - - expect(response.json()).toStrictEqual([]) - expect(response.statusCode).toEqual(200) - }) - it('as admin', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - - businessListMock.mockResolvedValueOnce([]) - const response = await app.inject() - .get(clusterContract.listClusters.path) - .end() - - expect(businessListMock).toHaveBeenCalledWith() - - expect(response.json()).toStrictEqual([]) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('getClusterDetails', () => { - it('should return cluster details', async () => { - const cluster: ClusterDetails = { - id: faker.string.uuid(), - clusterResources: true, - infos: '', - external: false, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - } - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetDetailsMock.mockResolvedValueOnce(cluster) - const response = await app.inject() - .get(clusterContract.getClusterDetails.path.replace(':clusterId', cluster.id)) - .end() - - expect(businessGetDetailsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(cluster) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(clusterContract.getClusterDetails.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetDetailsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getClusterUsage', () => { - it('should return cluster usage', async () => { - const resources = { - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - } - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetUsageMock.mockResolvedValueOnce(resources) - const response = await app.inject() - .get(clusterContract.getClusterUsage.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetUsageMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(resources) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(clusterContract.getClusterUsage.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetUsageMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getClusterEnvironments', () => { - it('should return cluster environments', async () => { - const envs: Environment[] = [] - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetEnvironmentsMock.mockResolvedValueOnce(envs) - const response = await app.inject() - .get(clusterContract.getClusterEnvironments.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(clusterContract.getClusterEnvironments.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('createCluster', () => { - const cluster: ClusterDetails = { - id: faker.string.uuid(), - clusterResources: true, - infos: '', - external: true, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - } - - it('should return created cluster', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(cluster) - const response = await app.inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end() - - expect(response.json()).toEqual(cluster) - expect(response.statusCode).toEqual(201) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('updateCluster', () => { - const clusterId = faker.string.uuid() - const cluster: Omit = { - clusterResources: true, - infos: '', - external: false, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - } - - it('should return created cluster', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: clusterId, ...cluster }) - const response = await app.inject() - .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) - .body(cluster) - .end() - - expect(response.json()).toEqual({ id: clusterId, ...cluster }) - expect(response.statusCode).toEqual(200) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) - .body(cluster) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) - .body(cluster) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('deleteCluster', () => { - it('should return empty when delete', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + describe('listClusters', () => { + it('as non admin', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + businessListMock.mockResolvedValueOnce([]); + const response = await app + .inject() + .get(clusterContract.listClusters.path) + .end(); + + expect(businessListMock).toHaveBeenCalledWith(user.user.id); + + expect(response.json()).toStrictEqual([]); + expect(response.statusCode).toEqual(200); + }); + it('as admin', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + + businessListMock.mockResolvedValueOnce([]); + const response = await app + .inject() + .get(clusterContract.listClusters.path) + .end(); + + expect(businessListMock).toHaveBeenCalledWith(); + + expect(response.json()).toStrictEqual([]); + expect(response.statusCode).toEqual(200); + }); + }); + + describe('getClusterDetails', () => { + it('should return cluster details', async () => { + const cluster: ClusterDetails = { + id: faker.string.uuid(), + clusterResources: true, + infos: '', + external: false, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + }; + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessGetDetailsMock.mockResolvedValueOnce(cluster); + const response = await app + .inject() + .get( + clusterContract.getClusterDetails.path.replace( + ':clusterId', + cluster.id, + ), + ) + .end(); + + expect(businessGetDetailsMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(cluster); + expect(response.statusCode).toEqual(200); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + clusterContract.getClusterDetails.path.replace( + ':clusterId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessGetDetailsMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('getClusterUsage', () => { + it('should return cluster usage', async () => { + const resources = { + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }), + }; + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessGetUsageMock.mockResolvedValueOnce(resources); + const response = await app + .inject() + .get( + clusterContract.getClusterUsage.path.replace( + ':clusterId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessGetUsageMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(resources); + expect(response.statusCode).toEqual(200); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + clusterContract.getClusterUsage.path.replace( + ':clusterId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessGetUsageMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('getClusterEnvironments', () => { + it('should return cluster environments', async () => { + const envs: Environment[] = []; + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessGetEnvironmentsMock.mockResolvedValueOnce(envs); + const response = await app + .inject() + .get( + clusterContract.getClusterEnvironments.path.replace( + ':clusterId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual([]); + expect(response.statusCode).toEqual(200); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + clusterContract.getClusterEnvironments.path.replace( + ':clusterId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('createCluster', () => { + const cluster: ClusterDetails = { + id: faker.string.uuid(), + clusterResources: true, + infos: '', + external: true, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + }; + + it('should return created cluster', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce(cluster); + const response = await app + .inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end(); + + expect(response.json()).toEqual(cluster); + expect(response.statusCode).toEqual(201); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end(); + + expect(response.statusCode).toEqual(403); + }); + }); + + describe('updateCluster', () => { + const clusterId = faker.string.uuid(); + const cluster: Omit = { + clusterResources: true, + infos: '', + external: false, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + }; + + it('should return created cluster', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce({ + id: clusterId, + ...cluster, + }); + const response = await app + .inject() + .put( + clusterContract.updateCluster.path.replace( + ':clusterId', + clusterId, + ), + ) + .body(cluster) + .end(); + + expect(response.json()).toEqual({ id: clusterId, ...cluster }); + expect(response.statusCode).toEqual(200); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .put( + clusterContract.updateCluster.path.replace( + ':clusterId', + clusterId, + ), + ) + .body(cluster) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + clusterContract.updateCluster.path.replace( + ':clusterId', + clusterId, + ), + ) + .body(cluster) + .end(); + + expect(response.statusCode).toEqual(403); + }); + }); + + describe('deleteCluster', () => { + it('should return empty when delete', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce(null); + const response = await app + .inject() + .delete( + clusterContract.deleteCluster.path.replace( + ':clusterId', + faker.string.uuid(), + ), + ) + .end(); + + expect(response.body).toBeFalsy(); + expect(response.statusCode).toEqual(204); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .delete( + clusterContract.deleteCluster.path.replace( + ':clusterId', + faker.string.uuid(), + ), + ) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + clusterContract.deleteCluster.path.replace( + ':clusterId', + faker.string.uuid(), + ), + ) + .end(); + + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts index d99c7aa1f..740cde7fc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts @@ -1,125 +1,144 @@ -import type { AsyncReturnType } from '@cpn-console/shared' -import { AdminAuthorized, clusterContract } from '@cpn-console/shared' +import type { AsyncReturnType } from '@cpn-console/shared'; +import { AdminAuthorized, clusterContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import '@old-server/types/index.js'; +import { authUser } from '@old-server/utils/controller.js'; import { - createCluster, - deleteCluster, - getClusterAssociatedEnvironments, - getClusterDetails as getClusterDetailsBusiness, - getClusterUsage, - listClusters, - updateCluster, -} from './business.js' -import '@old-server/types/index.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors.js' + ErrorResType, + Forbidden403, + Unauthorized401, +} from '@old-server/utils/errors.js'; + +import { + createCluster, + deleteCluster, + getClusterAssociatedEnvironments, + getClusterDetails as getClusterDetailsBusiness, + getClusterUsage, + listClusters, + updateCluster, +} from './business.js'; export function clusterRouter() { - return serverInstance.router(clusterContract, { - listClusters: async ({ request: req }) => { - const { adminPermissions, user } = await authUser(req) - - let body: AsyncReturnType = [] - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listClusters() - } else if (user) { - body = await listClusters(user.id) - } - - return { - status: 200, - body, - } - }, - - getClusterDetails: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const clusterId = params.clusterId - const cluster = await getClusterDetailsBusiness(clusterId) - - return { - status: 200, - body: cluster, - } - }, - - getClusterUsage: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const clusterId = params.clusterId - const usage = await getClusterUsage(clusterId) - - return { - status: 200, - body: usage, - } - }, - - createCluster: async ({ request: req, body: data }) => { - const { adminPermissions, user } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - - if (!user) return new Unauthorized401('Require to be requested from user not api key') - const body = await createCluster(data, user.id, req.id) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - getClusterEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const clusterId = params.clusterId - const environments = await getClusterAssociatedEnvironments(clusterId) - - return { - status: 200, - body: environments, - } - }, - - updateCluster: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user) return new Unauthorized401('Require to be requested from user not api key') - - const clusterId = params.clusterId - const body = await updateCluster(data, clusterId, user.id, req.id) - - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - deleteCluster: async ({ request: req, params, query: { force } }) => { - const { user, adminPermissions, tokenId } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user?.id && !tokenId) return new Unauthorized401('Your identity has not been found') - - const clusterId = params.clusterId - const body = await deleteCluster({ - clusterId, - userId: user?.id, - requestId: req.id, - force, - }) - - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - }) + return serverInstance.router(clusterContract, { + listClusters: async ({ request: req }) => { + const { adminPermissions, user } = await authUser(req); + + let body: AsyncReturnType = []; + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listClusters(); + } else if (user) { + body = await listClusters(user.id); + } + + return { + status: 200, + body, + }; + }, + + getClusterDetails: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const cluster = await getClusterDetailsBusiness(clusterId); + + return { + status: 200, + body: cluster, + }; + }, + + getClusterUsage: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const usage = await getClusterUsage(clusterId); + + return { + status: 200, + body: usage, + }; + }, + + createCluster: async ({ request: req, body: data }) => { + const { adminPermissions, user } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + const body = await createCluster(data, user.id, req.id); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + getClusterEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const environments = + await getClusterAssociatedEnvironments(clusterId); + + return { + status: 200, + body: environments, + }; + }, + + updateCluster: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + + const clusterId = params.clusterId; + const body = await updateCluster(data, clusterId, user.id, req.id); + + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + deleteCluster: async ({ request: req, params, query: { force } }) => { + const { user, adminPermissions, tokenId } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user?.id && !tokenId) + return new Unauthorized401('Your identity has not been found'); + + const clusterId = params.clusterId; + const body = await deleteCluster({ + clusterId, + userId: user?.id, + requestId: req.id, + force, + }); + + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts index b1185efb7..1d522f367 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts @@ -1,353 +1,441 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Cluster, Environment, Project, ProjectMembers, ProjectRole, Stage, User } from '@prisma/client' -import prisma from '../../__mocks__/prisma.js' -import { hook } from '../../__mocks__/utils/hook-wrapper.ts' -import { checkClusterResources, checkProjectResources, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business.ts' -import { Result } from '../../utils/business.js' +import { faker } from '@faker-js/faker'; +import type { + Cluster, + Environment, + Project, + ProjectMembers, + ProjectRole, + Stage, + User, +} from '@prisma/client'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import { hook } from '../../__mocks__/utils/hook-wrapper.ts'; +import { Result } from '../../utils/business.js'; +import { + checkClusterResources, + checkProjectResources, + createEnvironment, + deleteEnvironment, + getProjectEnvironments, + updateEnvironment, +} from './business.ts'; vi.mock('../../utils/hook-wrapper.ts', async () => ({ - hook, -})) + hook, +})); const user: User = { - id: faker.string.uuid(), - createdAt: new Date(), - updatedAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - adminRoleIds: [], - type: 'human', - lastLogin: null, -} + id: faker.string.uuid(), + createdAt: new Date(), + updatedAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + adminRoleIds: [], + type: 'human', + lastLogin: null, +}; const project: Project & { - clusters: Pick[] - members: ProjectMembers[] - roles: ProjectRole[] - owner: User + clusters: Pick[]; + members: ProjectMembers[]; + roles: ProjectRole[]; + owner: User; } = { - createdAt: new Date(), - updatedAt: new Date(), - description: '', - everyonePerms: 649n, - id: faker.string.uuid(), - locked: false, - name: faker.string.alphanumeric(8), - status: 'created', - ownerId: faker.string.uuid(), - owner: user, - limitless: false, - hprodCpu: faker.number.int({ min: 0, max: 1000 }), - hprodGpu: faker.number.int({ min: 0, max: 1000 }), - hprodMemory: faker.number.int({ min: 0, max: 1000 }), - prodCpu: faker.number.int({ min: 0, max: 1000 }), - prodGpu: faker.number.int({ min: 0, max: 1000 }), - prodMemory: faker.number.int({ min: 0, max: 1000 }), - clusters: [], - roles: [], - members: [], - slug: faker.string.alphanumeric(8), - lastSuccessProvisionningVersion: faker.string.numeric(), -} + createdAt: new Date(), + updatedAt: new Date(), + description: '', + everyonePerms: 649n, + id: faker.string.uuid(), + locked: false, + name: faker.string.alphanumeric(8), + status: 'created', + ownerId: faker.string.uuid(), + owner: user, + limitless: false, + hprodCpu: faker.number.int({ min: 0, max: 1000 }), + hprodGpu: faker.number.int({ min: 0, max: 1000 }), + hprodMemory: faker.number.int({ min: 0, max: 1000 }), + prodCpu: faker.number.int({ min: 0, max: 1000 }), + prodGpu: faker.number.int({ min: 0, max: 1000 }), + prodMemory: faker.number.int({ min: 0, max: 1000 }), + clusters: [], + roles: [], + members: [], + slug: faker.string.alphanumeric(8), + lastSuccessProvisionningVersion: faker.string.numeric(), +}; describe('test environment business', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('getProjectEnvironments', () => { - it('should query environment for projectId', async () => { - prisma.environment.findMany.mockResolvedValue([]) - const projectId = faker.string.uuid() - await getProjectEnvironments(projectId) - - expect(prisma.environment.findMany).toHaveBeenCalledTimes(1) - }) - }) - - describe('createEnvironment', () => { - const clusterId = faker.string.uuid() - const stageId = faker.string.uuid() - const env = { name: 'new-env' } - it('should create environment and trigger hook', async () => { - const requestId = faker.string.uuid() - const stageId = faker.string.uuid() - - prisma.environment.create.mockResolvedValue({ clusterId } as Environment) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const result = await createEnvironment({ - userId: user.id, - projectId: project.id, - name: env.name, - cpu: 0.1, - gpu: 0.5, - memory: 2.0, - clusterId, - stageId, - requestId, - }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - - it('should create environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid() - - prisma.environment.create.mockResolvedValue({ clusterId } as Environment) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const result = await createEnvironment({ - userId: user.id, - projectId: project.id, - name: env.name, - cpu: 0.1, - gpu: 0.5, - memory: 2.0, - clusterId, - stageId, - requestId, - }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - }) - }) - - describe('updateEnvironment', () => { - it('should update environment and trigger hook', async () => { - const requestId = faker.string.uuid() - const environmentId = faker.string.uuid() - - prisma.environment.update.mockResolvedValue({ projectId: project.id } as Environment) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const result = await updateEnvironment({ - user, - environmentId, - requestId, - cpu: 2.0, - gpu: 4.0, - memory: 12.5, - }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.update).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - - it('should update environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid() - const environmentId = faker.string.uuid() - - prisma.environment.update.mockResolvedValue({ projectId: project.id } as Environment) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const result = await updateEnvironment({ - user, - environmentId, - requestId, - cpu: 2.0, - gpu: 4.0, - memory: 12.5, - }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.update).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - }) - }) - - describe('deleteEnvironment', () => { - it('should delete environment and trigger hook', async () => { - const requestId = faker.string.uuid() - const environmentId = faker.string.uuid() - - prisma.environment.delete.mockResolvedValue({ projectId: project.id } as Environment) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const result = await deleteEnvironment({ environmentId, userId: user.id, projectId: project.id, requestId }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.delete).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - - it('should delete environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid() - const environmentId = faker.string.uuid() - - prisma.environment.delete.mockResolvedValue({ projectId: project.id } as Environment) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const result = await deleteEnvironment({ environmentId, userId: user.id, projectId: project.id, requestId }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.delete).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - }) - }) - - describe('checkClusterResources', () => { - it('should authorize cluster not yet configured', async () => { - const cluster: Cluster = { - cpu: 0, - gpu: 0, - memory: 0, - } as Cluster - const result = await checkClusterResources({ cpu: 1, gpu: 0, memory: 1 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - it('should authorize cluster not yet used', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 0, - gpu: 0, - memory: 0, - }, - } as any) - const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 7 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - it('should authorize cluster used but not full', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 2, - gpu: 0, - memory: 2, - }, - } as any) - const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 6 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - it('should refuse cluster without enough space', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 5, - gpu: 0, - memory: 5, - }, - } as any) - const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 6 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - expect(result.error).toEqual('Le cluster ne dispose pas de suffisamment de ressources : CPU, Mémoire.') - }) - it('should refuse cluster without GPU', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 2, - gpu: 0, - memory: 2, - }, - } as any) - const result = await checkClusterResources({ cpu: 2, gpu: 1, memory: 2 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - expect(result.error).toEqual('Le cluster ne dispose pas de suffisamment de ressources : GPU.') - }) - }) - - describe('checkProjectResources', () => { - const prodStage: Stage = { - id: faker.string.uuid(), - name: 'prod', - } - const hprodStage: Stage = { - id: faker.string.uuid(), - name: 'hprod', - } - it('should authorize prod deployment for project with hprod resource but no prod resources', async () => { - const project: Project = { - hprodCpu: 10, - hprodGpu: 10, - hprodMemory: 10, - prodCpu: 0, - prodGpu: 0, - prodMemory: 0, - } as Project - prisma.stage.findUnique.mockResolvedValue(prodStage) - prisma.stage.findMany.mockResolvedValue([prodStage]) - const result = await checkProjectResources({ cpu: 1, gpu: 0, memory: 1, stageId: prodStage.id }, project) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - it('should refuse hprod deployment for project with hprod resource but no prod resources', async () => { - const project: Project = { - hprodCpu: 10, - hprodGpu: 10, - hprodMemory: 10, - prodCpu: 0, - prodGpu: 0, - prodMemory: 0, - } as Project - prisma.stage.findUnique.mockResolvedValue(hprodStage) - prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]) - prisma.environment.aggregate.mockResolvedValue({ - _sum: { cpu: 0, gpu: 0, memory: 0 }, - } as any) - const result = await checkProjectResources({ cpu: 20, gpu: 20, memory: 20, stageId: hprodStage.id }, project) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - expect(result.error).toEqual('Le projet ne dispose pas de suffisamment de ressources : CPU, GPU, Mémoire.') - }) - it('should refuse overloading hprod deployment', async () => { - const project: Project = { - hprodCpu: 20, - hprodGpu: 20, - hprodMemory: 20, - prodCpu: 10, - prodGpu: 10, - prodMemory: 10, - } as Project - prisma.stage.findUnique.mockResolvedValue(hprodStage) - prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]) - prisma.environment.aggregate.mockResolvedValue({ - _sum: { cpu: 15, gpu: 15, memory: 15 }, - } as any) - const result = await checkProjectResources({ cpu: 5, gpu: 6, memory: 5, stageId: hprodStage.id }, project) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - expect(result.error).toEqual('Le projet ne dispose pas de suffisamment de ressources : GPU.') - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('getProjectEnvironments', () => { + it('should query environment for projectId', async () => { + prisma.environment.findMany.mockResolvedValue([]); + const projectId = faker.string.uuid(); + await getProjectEnvironments(projectId); + + expect(prisma.environment.findMany).toHaveBeenCalledTimes(1); + }); + }); + + describe('createEnvironment', () => { + const clusterId = faker.string.uuid(); + const stageId = faker.string.uuid(); + const env = { name: 'new-env' }; + it('should create environment and trigger hook', async () => { + const requestId = faker.string.uuid(); + const stageId = faker.string.uuid(); + + prisma.environment.create.mockResolvedValue({ + clusterId, + } as Environment); + hook.project.upsert.mockResolvedValue({ + results: {}, + project: { ...project }, + }); + + const result = await createEnvironment({ + userId: user.id, + projectId: project.id, + name: env.name, + cpu: 0.1, + gpu: 0.5, + memory: 2.0, + clusterId, + stageId, + requestId, + }); + + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(prisma.environment.create).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeTruthy(); + }); + + it('should create environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid(); + + prisma.environment.create.mockResolvedValue({ + clusterId, + } as Environment); + hook.project.upsert.mockResolvedValue({ + results: { failed: true }, + project: { ...project }, + }); + + const result = await createEnvironment({ + userId: user.id, + projectId: project.id, + name: env.name, + cpu: 0.1, + gpu: 0.5, + memory: 2.0, + clusterId, + stageId, + requestId, + }); + + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(prisma.environment.create).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeFalsy(); + }); + }); + + describe('updateEnvironment', () => { + it('should update environment and trigger hook', async () => { + const requestId = faker.string.uuid(); + const environmentId = faker.string.uuid(); + + prisma.environment.update.mockResolvedValue({ + projectId: project.id, + } as Environment); + hook.project.upsert.mockResolvedValue({ + results: {}, + project: { ...project }, + }); + + const result = await updateEnvironment({ + user, + environmentId, + requestId, + cpu: 2.0, + gpu: 4.0, + memory: 12.5, + }); + + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(prisma.environment.update).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeTruthy(); + }); + + it('should update environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid(); + const environmentId = faker.string.uuid(); + + prisma.environment.update.mockResolvedValue({ + projectId: project.id, + } as Environment); + hook.project.upsert.mockResolvedValue({ + results: { failed: true }, + project: { ...project }, + }); + + const result = await updateEnvironment({ + user, + environmentId, + requestId, + cpu: 2.0, + gpu: 4.0, + memory: 12.5, + }); + + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(prisma.environment.update).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeFalsy(); + }); + }); + + describe('deleteEnvironment', () => { + it('should delete environment and trigger hook', async () => { + const requestId = faker.string.uuid(); + const environmentId = faker.string.uuid(); + + prisma.environment.delete.mockResolvedValue({ + projectId: project.id, + } as Environment); + hook.project.upsert.mockResolvedValue({ + results: {}, + project: { ...project }, + }); + + const result = await deleteEnvironment({ + environmentId, + userId: user.id, + projectId: project.id, + requestId, + }); + + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(prisma.environment.delete).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeTruthy(); + }); + + it('should delete environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid(); + const environmentId = faker.string.uuid(); + + prisma.environment.delete.mockResolvedValue({ + projectId: project.id, + } as Environment); + hook.project.upsert.mockResolvedValue({ + results: { failed: true }, + project: { ...project }, + }); + + const result = await deleteEnvironment({ + environmentId, + userId: user.id, + projectId: project.id, + requestId, + }); + + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(prisma.environment.delete).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeFalsy(); + }); + }); + + describe('checkClusterResources', () => { + it('should authorize cluster not yet configured', async () => { + const cluster: Cluster = { + cpu: 0, + gpu: 0, + memory: 0, + } as Cluster; + const result = await checkClusterResources( + { cpu: 1, gpu: 0, memory: 1 }, + cluster, + ); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeTruthy(); + }); + it('should authorize cluster not yet used', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster; + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 0, + gpu: 0, + memory: 0, + }, + } as any); + const result = await checkClusterResources( + { cpu: 8, gpu: 0, memory: 7 }, + cluster, + ); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeTruthy(); + }); + it('should authorize cluster used but not full', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster; + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 2, + gpu: 0, + memory: 2, + }, + } as any); + const result = await checkClusterResources( + { cpu: 8, gpu: 0, memory: 6 }, + cluster, + ); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeTruthy(); + }); + it('should refuse cluster without enough space', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster; + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 5, + gpu: 0, + memory: 5, + }, + } as any); + const result = await checkClusterResources( + { cpu: 8, gpu: 0, memory: 6 }, + cluster, + ); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeFalsy(); + expect(result.error).toEqual( + 'Le cluster ne dispose pas de suffisamment de ressources : CPU, Mémoire.', + ); + }); + it('should refuse cluster without GPU', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster; + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 2, + gpu: 0, + memory: 2, + }, + } as any); + const result = await checkClusterResources( + { cpu: 2, gpu: 1, memory: 2 }, + cluster, + ); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeFalsy(); + expect(result.error).toEqual( + 'Le cluster ne dispose pas de suffisamment de ressources : GPU.', + ); + }); + }); + + describe('checkProjectResources', () => { + const prodStage: Stage = { + id: faker.string.uuid(), + name: 'prod', + }; + const hprodStage: Stage = { + id: faker.string.uuid(), + name: 'hprod', + }; + it('should authorize prod deployment for project with hprod resource but no prod resources', async () => { + const project: Project = { + hprodCpu: 10, + hprodGpu: 10, + hprodMemory: 10, + prodCpu: 0, + prodGpu: 0, + prodMemory: 0, + } as Project; + prisma.stage.findUnique.mockResolvedValue(prodStage); + prisma.stage.findMany.mockResolvedValue([prodStage]); + const result = await checkProjectResources( + { cpu: 1, gpu: 0, memory: 1, stageId: prodStage.id }, + project, + ); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeTruthy(); + }); + it('should refuse hprod deployment for project with hprod resource but no prod resources', async () => { + const project: Project = { + hprodCpu: 10, + hprodGpu: 10, + hprodMemory: 10, + prodCpu: 0, + prodGpu: 0, + prodMemory: 0, + } as Project; + prisma.stage.findUnique.mockResolvedValue(hprodStage); + prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]); + prisma.environment.aggregate.mockResolvedValue({ + _sum: { cpu: 0, gpu: 0, memory: 0 }, + } as any); + const result = await checkProjectResources( + { cpu: 20, gpu: 20, memory: 20, stageId: hprodStage.id }, + project, + ); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeFalsy(); + expect(result.error).toEqual( + 'Le projet ne dispose pas de suffisamment de ressources : CPU, GPU, Mémoire.', + ); + }); + it('should refuse overloading hprod deployment', async () => { + const project: Project = { + hprodCpu: 20, + hprodGpu: 20, + hprodMemory: 20, + prodCpu: 10, + prodGpu: 10, + prodMemory: 10, + } as Project; + prisma.stage.findUnique.mockResolvedValue(hprodStage); + prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]); + prisma.environment.aggregate.mockResolvedValue({ + _sum: { cpu: 15, gpu: 15, memory: 15 }, + } as any); + const result = await checkProjectResources( + { cpu: 5, gpu: 6, memory: 5, stageId: hprodStage.id }, + project, + ); + expect(result).toBeInstanceOf(Result); + expect(result.success).toBeFalsy(); + expect(result.error).toEqual( + 'Le projet ne dispose pas de suffisamment de ressources : GPU.', + ); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts index 17ae7bd7b..a28489487 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts @@ -1,300 +1,388 @@ -import type { Cluster, Environment, Project, Stage, User } from '@prisma/client' +import prisma from '@old-server/prisma.js'; import { - addLogs, - deleteEnvironment as deleteEnvironmentQuery, - getEnvironmentsByProjectId, - initializeEnvironment, - updateEnvironment as updateEnvironmentQuery, -} from '@old-server/resources/queries-index.js' -import type { Resources, UserDetails } from '@old-server/types/index.js' -import { hook } from '@old-server/utils/hook-wrapper.js' -import prisma from '@old-server/prisma.js' -import { Result } from '@old-server/utils/business.js' + addLogs, + deleteEnvironment as deleteEnvironmentQuery, + getEnvironmentsByProjectId, + initializeEnvironment, + updateEnvironment as updateEnvironmentQuery, +} from '@old-server/resources/queries-index.js'; +import type { Resources, UserDetails } from '@old-server/types/index.js'; +import { Result } from '@old-server/utils/business.js'; +import { hook } from '@old-server/utils/hook-wrapper.js'; +import type { + Cluster, + Environment, + Project, + Stage, + User, +} from '@prisma/client'; export function getProjectEnvironments(projectId: Project['id']) { - return getEnvironmentsByProjectId(projectId) + return getEnvironmentsByProjectId(projectId); } // Routes logic interface CreateEnvironmentParam { - userId: User['id'] - projectId: Project['id'] - name: Environment['name'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] - clusterId: Environment['clusterId'] - stageId: Stage['id'] - requestId: string + userId: User['id']; + projectId: Project['id']; + name: Environment['name']; + cpu: Environment['cpu']; + gpu: Environment['gpu']; + memory: Environment['memory']; + clusterId: Environment['clusterId']; + stageId: Stage['id']; + requestId: string; } interface CreateEnvironmentResult { - id: Environment['id'] - createdAt: Date - updatedAt: Date - projectId: Project['id'] - name: Environment['name'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] - clusterId: Environment['clusterId'] - stageId: Stage['id'] + id: Environment['id']; + createdAt: Date; + updatedAt: Date; + projectId: Project['id']; + name: Environment['name']; + cpu: Environment['cpu']; + gpu: Environment['gpu']; + memory: Environment['memory']; + clusterId: Environment['clusterId']; + stageId: Stage['id']; } export async function createEnvironment({ - userId, - projectId, - name, - cpu, - gpu, - memory, - clusterId, - stageId, - requestId, + userId, + projectId, + name, + cpu, + gpu, + memory, + clusterId, + stageId, + requestId, }: CreateEnvironmentParam): Promise> { - const environment = await initializeEnvironment({ projectId, name, cpu, gpu, memory, clusterId, stageId }) + const environment = await initializeEnvironment({ + projectId, + name, + cpu, + gpu, + memory, + clusterId, + stageId, + }); - const { results } = await hook.project.upsert(projectId) - await addLogs({ action: 'Create Environment', data: results, userId, requestId, projectId }) - if (results.failed) { - return Result.fail('Echec des services à la création de l\'environnement') - } + const { results } = await hook.project.upsert(projectId); + await addLogs({ + action: 'Create Environment', + data: results, + userId, + requestId, + projectId, + }); + if (results.failed) { + return Result.fail( + "Echec des services à la création de l'environnement", + ); + } - return Result.succeed({ - ...environment, - stageId, - }) + return Result.succeed({ + ...environment, + stageId, + }); } interface UpdateEnvironmentParam { - user: UserDetails - environmentId: Environment['id'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] - requestId: string + user: UserDetails; + environmentId: Environment['id']; + cpu: Environment['cpu']; + gpu: Environment['gpu']; + memory: Environment['memory']; + requestId: string; } export async function updateEnvironment({ - user, - environmentId, - requestId, - cpu, - gpu, - memory, -}: UpdateEnvironmentParam) { - const env = await updateEnvironmentQuery({ - id: environmentId, + user, + environmentId, + requestId, cpu, gpu, memory, - }) - const { results } = await hook.project.upsert(env.projectId) - await addLogs({ action: 'Update Environment', data: results, userId: user.id, requestId, projectId: env.projectId }) - if (results.failed) { - return Result.fail('Echec des services à la mise à jour de l\'environnement') - } +}: UpdateEnvironmentParam) { + const env = await updateEnvironmentQuery({ + id: environmentId, + cpu, + gpu, + memory, + }); + const { results } = await hook.project.upsert(env.projectId); + await addLogs({ + action: 'Update Environment', + data: results, + userId: user.id, + requestId, + projectId: env.projectId, + }); + if (results.failed) { + return Result.fail( + "Echec des services à la mise à jour de l'environnement", + ); + } - return Result.succeed(env) + return Result.succeed(env); } interface DeleteEnvironmentParam { - userId?: User['id'] - environmentId: Environment['id'] - projectId: Project['id'] - requestId: string + userId?: User['id']; + environmentId: Environment['id']; + projectId: Project['id']; + requestId: string; } export async function deleteEnvironment({ - userId, - environmentId, - projectId, - requestId, + userId, + environmentId, + projectId, + requestId, }: DeleteEnvironmentParam) { - const env = await deleteEnvironmentQuery(environmentId) + const env = await deleteEnvironmentQuery(environmentId); - const { results } = await hook.project.upsert(projectId) - await addLogs({ action: 'Delete Environment', data: results, userId, requestId, projectId: env.projectId }) - if (results.failed) { - return Result.fail('Echec des services à la suppression de l\'environnement') - } - return Result.succeed(null) + const { results } = await hook.project.upsert(projectId); + await addLogs({ + action: 'Delete Environment', + data: results, + userId, + requestId, + projectId: env.projectId, + }); + if (results.failed) { + return Result.fail( + "Echec des services à la suppression de l'environnement", + ); + } + return Result.succeed(null); } export async function checkEnvironmentCreate(input: { - clusterId: Cluster['id'] - projectId: Project['id'] - name: Environment['name'] - stageId: Stage['id'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] + clusterId: Cluster['id']; + projectId: Project['id']; + name: Environment['name']; + stageId: Stage['id']; + cpu: Environment['cpu']; + gpu: Environment['gpu']; + memory: Environment['memory']; }): Promise> { - const errorMessages: string[] = [] - const [stage, sameNameEnvironment, cluster] = await Promise.all([ - input.stageId - ? prisma.stage.findUnique({ where: { id: input.stageId } }) - : undefined, - input.name - ? prisma.environment.findUnique({ where: { projectId_name: { projectId: input.projectId, name: input.name } } }) - : undefined, - input.clusterId - ? prisma.cluster.findFirst({ - where: { - OR: [{ // un cluster public - id: input.clusterId, - privacy: 'public', - }, { - id: input.clusterId, // un cluster dédié rattaché au projet - privacy: 'dedicated', - projects: { some: { id: input.projectId } }, - }], - }, - }) - : undefined, - ]) - if (sameNameEnvironment) errorMessages.push('Ce nom d\'environnement est déjà pris.') - if (!stage) errorMessages.push('Type d\'environnment invalide.') - if (!cluster) { - errorMessages.push('Cluster invalide.') - } else { - const resourceCheckResult = await checkClusterResources(input, cluster) - if (resourceCheckResult.isError) { - errorMessages.push(resourceCheckResult.error) + const errorMessages: string[] = []; + const [stage, sameNameEnvironment, cluster] = await Promise.all([ + input.stageId + ? prisma.stage.findUnique({ where: { id: input.stageId } }) + : undefined, + input.name + ? prisma.environment.findUnique({ + where: { + projectId_name: { + projectId: input.projectId, + name: input.name, + }, + }, + }) + : undefined, + input.clusterId + ? prisma.cluster.findFirst({ + where: { + OR: [ + { + // un cluster public + id: input.clusterId, + privacy: 'public', + }, + { + id: input.clusterId, // un cluster dédié rattaché au projet + privacy: 'dedicated', + projects: { some: { id: input.projectId } }, + }, + ], + }, + }) + : undefined, + ]); + if (sameNameEnvironment) + errorMessages.push("Ce nom d'environnement est déjà pris."); + if (!stage) errorMessages.push("Type d'environnment invalide."); + if (!cluster) { + errorMessages.push('Cluster invalide.'); + } else { + const resourceCheckResult = await checkClusterResources(input, cluster); + if (resourceCheckResult.isError) { + errorMessages.push(resourceCheckResult.error); + } + const project = await prisma.project.findUniqueOrThrow({ + where: { id: input.projectId }, + }); + const projectCheckResult = await checkProjectResources(input, project); + if (projectCheckResult.isError) { + errorMessages.push(projectCheckResult.error); + } } - const project = await prisma.project.findUniqueOrThrow({ where: { id: input.projectId } }) - const projectCheckResult = await checkProjectResources(input, project) - if (projectCheckResult.isError) { - errorMessages.push(projectCheckResult.error) + if (errorMessages.length > 0) { + return Result.fail(errorMessages.join('\n')); } - } - if (errorMessages.length > 0) { - return Result.fail(errorMessages.join('\n')) - } - return Result.succeed(true) + return Result.succeed(true); } -export async function checkClusterResources(input: { - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] -}, cluster: Cluster): Promise> { - if (cluster.cpu === 0 && cluster.memory === 0) { - // Unconfigured cluster - return Result.succeed(true) - } - const unsufficientResource = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { cpu: cluster.cpu, gpu: cluster.gpu, memory: cluster.memory }, - where: { - cluster: { - id: cluster.id, - }, +export async function checkClusterResources( + input: { + cpu: Environment['cpu']; + gpu: Environment['gpu']; + memory: Environment['memory']; }, - }) - if (unsufficientResource.length > 0) { - return Result.fail(`Le cluster ne dispose pas de suffisamment de ressources : ${unsufficientResource.join(', ')}.`) - } - return Result.succeed(true) + cluster: Cluster, +): Promise> { + if (cluster.cpu === 0 && cluster.memory === 0) { + // Unconfigured cluster + return Result.succeed(true); + } + const unsufficientResource = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { cpu: cluster.cpu, gpu: cluster.gpu, memory: cluster.memory }, + where: { + cluster: { + id: cluster.id, + }, + }, + }); + if (unsufficientResource.length > 0) { + return Result.fail( + `Le cluster ne dispose pas de suffisamment de ressources : ${unsufficientResource.join(', ')}.`, + ); + } + return Result.succeed(true); } -export async function checkProjectResources(input: { - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] - stageId: Environment['stageId'] -}, project: Project): Promise> { - if (project.limitless) { - // No limits - return Result.succeed(true) - } - const stage = await prisma.stage.findUnique({ where: { id: input.stageId } }) - const prodStages = await prisma.stage.findMany({ select: { id: true }, where: { name: 'prod' } }) - let overflowResources: string[] - if (stage?.name === 'prod') { - overflowResources = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { cpu: project.prodCpu, gpu: project.prodGpu, memory: project.prodMemory }, - where: { - projectId: project.id, - stageId: { - in: prodStages.map(s => s.id), - }, - }, - }) - } else { // hprod - overflowResources = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { cpu: project.hprodCpu, gpu: project.hprodGpu, memory: project.hprodMemory }, - where: { - projectId: project.id, - stageId: { - notIn: prodStages.map(s => s.id), - }, - }, - }) - } - if (overflowResources.length > 0) { - return Result.fail(`Le projet ne dispose pas de suffisamment de ressources : ${overflowResources.join(', ')}.`) - } - return Result.succeed(true) +export async function checkProjectResources( + input: { + cpu: Environment['cpu']; + gpu: Environment['gpu']; + memory: Environment['memory']; + stageId: Environment['stageId']; + }, + project: Project, +): Promise> { + if (project.limitless) { + // No limits + return Result.succeed(true); + } + const stage = await prisma.stage.findUnique({ + where: { id: input.stageId }, + }); + const prodStages = await prisma.stage.findMany({ + select: { id: true }, + where: { name: 'prod' }, + }); + let overflowResources: string[]; + if (stage?.name === 'prod') { + overflowResources = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { + cpu: project.prodCpu, + gpu: project.prodGpu, + memory: project.prodMemory, + }, + where: { + projectId: project.id, + stageId: { + in: prodStages.map((s) => s.id), + }, + }, + }); + } else { + // hprod + overflowResources = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { + cpu: project.hprodCpu, + gpu: project.hprodGpu, + memory: project.hprodMemory, + }, + where: { + projectId: project.id, + stageId: { + notIn: prodStages.map((s) => s.id), + }, + }, + }); + } + if (overflowResources.length > 0) { + return Result.fail( + `Le projet ne dispose pas de suffisamment de ressources : ${overflowResources.join(', ')}.`, + ); + } + return Result.succeed(true); } export async function checkEnvironmentUpdate(input: { - environmentId: Environment['id'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] + environmentId: Environment['id']; + cpu: Environment['cpu']; + gpu: Environment['gpu']; + memory: Environment['memory']; }): Promise> { - const environment = await prisma.environment.findUniqueOrThrow({ - select: { cluster: true, projectId: true, stageId: true }, - where: { id: input.environmentId }, - }) - const cluster = await prisma.cluster.findUniqueOrThrow({ - where: { id: environment.cluster.id }, - }) - const errorMessages: string[] = [] - const resourceCheckResult = await checkClusterResources(input, cluster) - if (resourceCheckResult.isError) { - errorMessages.push(resourceCheckResult.error) - } - const project = await prisma.project.findUniqueOrThrow({ where: { id: environment.projectId } }) - const projectCheckResult = await checkProjectResources({ stageId: environment.stageId, ...input }, project) - if (projectCheckResult.isError) { - errorMessages.push(projectCheckResult.error) - } - if (errorMessages.length > 0) { - return Result.fail(errorMessages.join('\n')) - } - return Result.succeed(true) + const environment = await prisma.environment.findUniqueOrThrow({ + select: { cluster: true, projectId: true, stageId: true }, + where: { id: input.environmentId }, + }); + const cluster = await prisma.cluster.findUniqueOrThrow({ + where: { id: environment.cluster.id }, + }); + const errorMessages: string[] = []; + const resourceCheckResult = await checkClusterResources(input, cluster); + if (resourceCheckResult.isError) { + errorMessages.push(resourceCheckResult.error); + } + const project = await prisma.project.findUniqueOrThrow({ + where: { id: environment.projectId }, + }); + const projectCheckResult = await checkProjectResources( + { stageId: environment.stageId, ...input }, + project, + ); + if (projectCheckResult.isError) { + errorMessages.push(projectCheckResult.error); + } + if (errorMessages.length > 0) { + return Result.fail(errorMessages.join('\n')); + } + return Result.succeed(true); } -export async function getOverflowResources({ request, limit, where }: { - request: Resources - limit: Resources - where: any -}): Promise { - if (limit.cpu === 0 && limit.memory === 0) { - // Unconfigured project prod resources - return [] - } - const environmentResources = await prisma.environment.aggregate({ - _sum: { - memory: true, - cpu: true, - gpu: true, - }, +export async function getOverflowResources({ + request, + limit, where, - }) - const unsufficientResource: string[] = [] - if ((environmentResources._sum.cpu ?? 0) + request.cpu > limit.cpu) { - unsufficientResource.push('CPU') - } - if ((environmentResources._sum.gpu ?? 0) + request.gpu > limit.gpu) { - unsufficientResource.push('GPU') - } - if ((environmentResources._sum.memory ?? 0) + request.memory > limit.memory) { - unsufficientResource.push('Mémoire') - } - return unsufficientResource +}: { + request: Resources; + limit: Resources; + where: any; +}): Promise { + if (limit.cpu === 0 && limit.memory === 0) { + // Unconfigured project prod resources + return []; + } + const environmentResources = await prisma.environment.aggregate({ + _sum: { + memory: true, + cpu: true, + gpu: true, + }, + where, + }); + const unsufficientResource: string[] = []; + if ((environmentResources._sum.cpu ?? 0) + request.cpu > limit.cpu) { + unsufficientResource.push('CPU'); + } + if ((environmentResources._sum.gpu ?? 0) + request.gpu > limit.gpu) { + unsufficientResource.push('GPU'); + } + if ( + (environmentResources._sum.memory ?? 0) + request.memory > + limit.memory + ) { + unsufficientResource.push('Mémoire'); + } + return unsufficientResource; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts index 019923c37..cc6f8b9e1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts @@ -1,98 +1,113 @@ -import type { Environment, Prisma, Project } from '@prisma/client' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { Environment, Prisma, Project } from '@prisma/client'; // SELECT export function getEnvironmentByIdOrThrow(id: Environment['id']) { - return prisma.environment.findUniqueOrThrow({ where: { id }, include: { stage: true } }) + return prisma.environment.findUniqueOrThrow({ + where: { id }, + include: { stage: true }, + }); } export function getEnvironmentInfos(id: Environment['id']) { - return prisma.environment.findUniqueOrThrow({ - where: { id }, - include: { - project: { - select: { - owner: true, - name: true, - id: true, - status: true, - repositories: { - where: { isInfra: true }, - }, - locked: true, - clusters: { - select: { - id: true, - label: true, - privacy: true, - clusterResources: true, + return prisma.environment.findUniqueOrThrow({ + where: { id }, + include: { + project: { + select: { + owner: true, + name: true, + id: true, + status: true, + repositories: { + where: { isInfra: true }, + }, + locked: true, + clusters: { + select: { + id: true, + label: true, + privacy: true, + clusterResources: true, + }, + }, + }, }, - }, + stage: true, }, - }, - stage: true, - }, - }) + }); } export async function getEnvironmentsByProjectId(projectId: Project['id']) { - return prisma.environment.findMany({ - where: { projectId }, - include: { - stage: true, - }, - }) + return prisma.environment.findMany({ + where: { projectId }, + include: { + stage: true, + }, + }); } export function getEnvironmentByIdWithCluster(id: Environment['id']) { - return prisma.environment.findUnique({ - where: { id }, - include: { - cluster: { - include: { kubeconfig: true }, - }, - }, - }) + return prisma.environment.findUnique({ + where: { id }, + include: { + cluster: { + include: { kubeconfig: true }, + }, + }, + }); } // INSERT -export function initializeEnvironment(data: Prisma.EnvironmentUncheckedCreateInput) { - return prisma.environment.create({ - data, - include: { - project: { +export function initializeEnvironment( + data: Prisma.EnvironmentUncheckedCreateInput, +) { + return prisma.environment.create({ + data, include: { - repositories: { - where: { isInfra: true }, - }, + project: { + include: { + repositories: { + where: { isInfra: true }, + }, + }, + }, }, - }, - }, - }) + }); } -export function updateEnvironment({ id, cpu, gpu, memory }: { id: Environment['id'], cpu: Environment['cpu'], gpu: Environment['gpu'], memory: Environment['memory'] }) { - return prisma.environment.update({ - where: { - id, - }, - data: { - cpu, - gpu, - memory, - }, - }) +export function updateEnvironment({ + id, + cpu, + gpu, + memory, +}: { + id: Environment['id']; + cpu: Environment['cpu']; + gpu: Environment['gpu']; + memory: Environment['memory']; +}) { + return prisma.environment.update({ + where: { + id, + }, + data: { + cpu, + gpu, + memory, + }, + }); } // DELETE export function deleteEnvironment(id: Environment['id']) { - return prisma.environment.delete({ - where: { id }, - }) + return prisma.environment.delete({ + where: { id }, + }); } export function deleteAllEnvironmentForProject(id: Project['id']) { - return prisma.environment.deleteMany({ - where: { projectId: id }, - }) + return prisma.environment.deleteMany({ + where: { projectId: id }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts index 47337bbb4..e8dfea278 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts @@ -1,372 +1,590 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { type Environment, PROJECT_PERMS, environmentContract } from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { atDates, getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetProjectEnvironmentsMock = vi.spyOn(business, 'getProjectEnvironments') -const businessCreateEnvironmentMock = vi.spyOn(business, 'createEnvironment') -const businessUpdateEnvironmentMock = vi.spyOn(business, 'updateEnvironment') -const businessDeleteEnvironmentMock = vi.spyOn(business, 'deleteEnvironment') -const businessCheckEnvironmentCreateMock = vi.spyOn(business, 'checkEnvironmentCreate') -const businessCheckEnvironmentUpdateMock = vi.spyOn(business, 'checkEnvironmentUpdate') +import { + type Environment, + PROJECT_PERMS, + environmentContract, +} from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { + atDates, + getProjectMockInfos, + getUserMockInfos, +} from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessGetProjectEnvironmentsMock = vi.spyOn( + business, + 'getProjectEnvironments', +); +const businessCreateEnvironmentMock = vi.spyOn(business, 'createEnvironment'); +const businessUpdateEnvironmentMock = vi.spyOn(business, 'updateEnvironment'); +const businessDeleteEnvironmentMock = vi.spyOn(business, 'deleteEnvironment'); +const businessCheckEnvironmentCreateMock = vi.spyOn( + business, + 'checkEnvironmentCreate', +); +const businessCheckEnvironmentUpdateMock = vi.spyOn( + business, + 'checkEnvironmentUpdate', +); describe('environmentRouter tests', () => { - let projectId: string - let environmentId: string - let environmentData: Omit - - beforeEach(() => { - vi.resetAllMocks() - projectId = faker.string.uuid() - environmentId = faker.string.uuid() - environmentData = { - projectId, - name: 'envname', - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - clusterId: faker.string.uuid(), - stageId: faker.string.uuid(), - } - }) - - describe('listEnvironments', () => { - it('should return environments for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetProjectEnvironmentsMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(environmentContract.listEnvironments.path) - .query({ projectId }) - .end() - - expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledWith(projectId) - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should return empty for non member of projectId query ', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(environmentContract.listEnvironments.path) - .query({ projectId }) - .end() - - expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledTimes(0) - expect(response.json()).toEqual([]) - }) - }) - - describe('createEnvironment', () => { - it('should create environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ success: true }) - businessCreateEnvironmentMock.mockResolvedValueOnce({ - success: true, - data: { id: environmentId, ...environmentData, ...atDates }, - }) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.json()).toMatchObject({ id: environmentId, ...environmentData }) - expect(response.statusCode).toEqual(201) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(403) - }) - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ success: true, message: 'pas d erreur' }) - businessCreateEnvironmentMock.mockResolvedValueOnce({ isError: true, message: 'une erreur' }) - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(500) - }) - it('should pass invalid reason error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ isError: true, message: 'une erreur' }) - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('updateEnvironment', () => { - let updateData: { cpu: number, gpu: number, memory: number } + let projectId: string; + let environmentId: string; + let environmentData: Omit; + beforeEach(() => { - updateData = { - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - } - }) - it('should update environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ success: true, value: true }) - businessUpdateEnvironmentMock.mockResolvedValueOnce({ success: true, data: { id: environmentId, ...environmentData, ...atDates } }) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.json()).toMatchObject({ id: environmentId, ...environmentData }) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 404 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(404) - }) - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateEnvironmentMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(500) - }) - it('should pass invalid reason error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('deleteEnvironment', () => { - it('should delete environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteEnvironmentMock.mockResolvedValueOnce({ success: true }) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(204) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteEnvironmentMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(500) - }) - }) -}) + vi.resetAllMocks(); + projectId = faker.string.uuid(); + environmentId = faker.string.uuid(); + environmentData = { + projectId, + name: 'envname', + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + clusterId: faker.string.uuid(), + stageId: faker.string.uuid(), + }; + }); + + describe('listEnvironments', () => { + it('should return environments for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessGetProjectEnvironmentsMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .get(environmentContract.listEnvironments.path) + .query({ projectId }) + .end(); + + expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledWith( + projectId, + ); + expect(response.statusCode).toEqual(200); + expect(response.json()).toEqual([]); + }); + + it('should return empty for non member of projectId query ', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(environmentContract.listEnvironments.path) + .query({ projectId }) + .end(); + + expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledTimes(0); + expect(response.json()).toEqual([]); + }); + }); + + describe('createEnvironment', () => { + it('should create environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ + success: true, + }); + businessCreateEnvironmentMock.mockResolvedValueOnce({ + success: true, + data: { id: environmentId, ...environmentData, ...atDates }, + }); + + const response = await app + .inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end(); + + expect(response.json()).toMatchObject({ + id: environmentId, + ...environmentData, + }); + expect(response.statusCode).toEqual(201); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end(); + + expect(response.statusCode).toEqual(403); + }); + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ + success: true, + message: 'pas d erreur', + }); + businessCreateEnvironmentMock.mockResolvedValueOnce({ + isError: true, + message: 'une erreur', + }); + const response = await app + .inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end(); + + expect(response.statusCode).toEqual(500); + }); + it('should pass invalid reason error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ + isError: true, + message: 'une erreur', + }); + const response = await app + .inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end(); + + expect(response.statusCode).toEqual(400); + }); + }); + + describe('updateEnvironment', () => { + let updateData: { cpu: number; gpu: number; memory: number }; + beforeEach(() => { + updateData = { + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ + min: 0, + max: 10, + fractionDigits: 1, + }), + }; + }); + it('should update environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ + success: true, + value: true, + }); + businessUpdateEnvironmentMock.mockResolvedValueOnce({ + success: true, + data: { id: environmentId, ...environmentData, ...atDates }, + }); + + const response = await app + .inject() + .put( + environmentContract.updateEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .body(updateData) + .end(); + + expect(response.json()).toMatchObject({ + id: environmentId, + ...environmentData, + }); + expect(response.statusCode).toEqual(200); + }); + + it('should return 403 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + environmentContract.updateEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + environmentContract.updateEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + environmentContract.updateEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + environmentContract.updateEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 404 if not permited', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + environmentContract.updateEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(404); + }); + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateEnvironmentMock.mockResolvedValueOnce({ + isError: true, + value: 'une erreur', + }); + const response = await app + .inject() + .put( + environmentContract.updateEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(500); + }); + it('should pass invalid reason error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ + isError: true, + value: 'une erreur', + }); + const response = await app + .inject() + .put( + environmentContract.updateEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(400); + }); + }); + + describe('deleteEnvironment', () => { + it('should delete environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteEnvironmentMock.mockResolvedValueOnce({ + success: true, + }); + + const response = await app + .inject() + .delete( + environmentContract.deleteEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(204); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + environmentContract.deleteEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + environmentContract.deleteEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + environmentContract.deleteEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + environmentContract.deleteEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteEnvironmentMock.mockResolvedValueOnce({ + isError: true, + value: 'une erreur', + }); + const response = await app + .inject() + .delete( + environmentContract.deleteEnvironment.path.replace( + ':environmentId', + environmentId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(500); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts index 8a5c8b495..b8ab3ee4a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts @@ -1,109 +1,150 @@ -import { ProjectAuthorized, environmentContract } from '@cpn-console/shared' -import { checkEnvironmentCreate, checkEnvironmentUpdate, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401 } from '@old-server/utils/errors.js' +import { ProjectAuthorized, environmentContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { + BadRequest400, + Forbidden403, + Internal500, + NotFound404, + Unauthorized401, +} from '@old-server/utils/errors.js'; + +import { + checkEnvironmentCreate, + checkEnvironmentUpdate, + createEnvironment, + deleteEnvironment, + getProjectEnvironments, + updateEnvironment, +} from './business.js'; export function environmentRouter() { - return serverInstance.router(environmentContract, { - listEnvironments: async ({ request: req, query }) => { - const projectId = query.projectId - const perms = await authUser(req, { id: projectId }) + return serverInstance.router(environmentContract, { + listEnvironments: async ({ request: req, query }) => { + const projectId = query.projectId; + const perms = await authUser(req, { id: projectId }); - const environments = ProjectAuthorized.ListEnvironments(perms) - ? await getProjectEnvironments(projectId) - : [] + const environments = ProjectAuthorized.ListEnvironments(perms) + ? await getProjectEnvironments(projectId) + : []; - return { - status: 200, - body: environments, - } - }, + return { + status: 200, + body: environments, + }; + }, - createEnvironment: async ({ request: req, body: requestBody }) => { - const projectId = requestBody.projectId - const perms = await authUser(req, { id: projectId }) + createEnvironment: async ({ request: req, body: requestBody }) => { + const projectId = requestBody.projectId; + const perms = await authUser(req, { id: projectId }); - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); - const checkCreateResult = await checkEnvironmentCreate({ ...requestBody }) - if (checkCreateResult.isError) return new BadRequest400(checkCreateResult.error) + const checkCreateResult = await checkEnvironmentCreate({ + ...requestBody, + }); + if (checkCreateResult.isError) + return new BadRequest400(checkCreateResult.error); - const result = await createEnvironment({ - userId: perms.user.id, - projectId, - name: requestBody.name, - clusterId: requestBody.clusterId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - stageId: requestBody.stageId, - requestId: req.id, - }) - if (result.isError) { - return new Internal500(result.error) - } - return { - status: 201, - body: result.data, - } - }, + const result = await createEnvironment({ + userId: perms.user.id, + projectId, + name: requestBody.name, + clusterId: requestBody.clusterId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + stageId: requestBody.stageId, + requestId: req.id, + }); + if (result.isError) { + return new Internal500(result.error); + } + return { + status: 201, + body: result.data, + }; + }, - updateEnvironment: async ({ request: req, body: requestBody, params }) => { - const { environmentId } = params - const perms = await authUser(req, { environmentId }) - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!ProjectAuthorized.ListEnvironments(perms)) return new NotFound404() - if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + updateEnvironment: async ({ + request: req, + body: requestBody, + params, + }) => { + const { environmentId } = params; + const perms = await authUser(req, { environmentId }); + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!ProjectAuthorized.ListEnvironments(perms)) + return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); - const checkUpdateResult = await checkEnvironmentUpdate({ environmentId, ...requestBody }) - if (checkUpdateResult.isError) return new BadRequest400(checkUpdateResult.error) + const checkUpdateResult = await checkEnvironmentUpdate({ + environmentId, + ...requestBody, + }); + if (checkUpdateResult.isError) + return new BadRequest400(checkUpdateResult.error); - const result = await updateEnvironment({ - user: perms.user, - environmentId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - requestId: req.id, - }) - if (result.isError) { - return new Internal500(result.error) - } - return { - status: 200, - body: result.data, - } - }, + const result = await updateEnvironment({ + user: perms.user, + environmentId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + requestId: req.id, + }); + if (result.isError) { + return new Internal500(result.error); + } + return { + status: 200, + body: result.data, + }; + }, - deleteEnvironment: async ({ request: req, params }) => { - const { environmentId } = params - const perms = await authUser(req, { environmentId }) - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + deleteEnvironment: async ({ request: req, params }) => { + const { environmentId } = params; + const perms = await authUser(req, { environmentId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); - const result = await deleteEnvironment({ - userId: perms.user?.id, - environmentId, - requestId: req.id, - projectId: perms.projectId, - }) - if (result.isError) { - return new Internal500(result.error) - } + const result = await deleteEnvironment({ + userId: perms.user?.id, + environmentId, + requestId: req.id, + projectId: perms.projectId, + }); + if (result.isError) { + return new Internal500(result.error); + } - return { - status: 204, - body: result.data, - } - }, - }) + return { + status: 204, + body: result.data, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts index 930f757ea..15735c4e8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts @@ -1,49 +1,93 @@ -import type { FastifyInstance } from 'fastify' -import { serverInstance } from '@old-server/app.js' +import { serverInstance } from '@old-server/app.js'; +import type { FastifyInstance } from 'fastify'; -import { adminRoleRouter } from './admin-role/router.js' -import { adminTokenRouter } from './admin-token/router.js' -import { clusterRouter } from './cluster/router.js' -import { environmentRouter } from './environment/router.js' -import { logRouter } from './log/router.js' -import { personalAccessTokenRouter } from './user/tokens/router.js' -import { pluginConfigRouter } from './system/config/router.js' -import { projectMemberRouter } from './project-member/router.js' -import { projectRoleRouter } from './project-role/router.js' -import { projectRouter } from './project/router.js' -import { projectServiceRouter } from './project-service/router.js' -import { repositoryRouter } from './repository/router.js' -import { serviceChainRouter } from './service-chain/router.js' -import { serviceMonitorRouter } from './service-monitor/router.js' -import { stageRouter } from './stage/router.js' -import { systemRouter } from './system/router.js' -import { systemSettingsRouter } from './system/settings/router.js' -import { userRouter } from './user/router.js' -import { zoneRouter } from './zone/router.js' +import { adminRoleRouter } from './admin-role/router.js'; +import { adminTokenRouter } from './admin-token/router.js'; +import { clusterRouter } from './cluster/router.js'; +import { environmentRouter } from './environment/router.js'; +import { logRouter } from './log/router.js'; +import { projectMemberRouter } from './project-member/router.js'; +import { projectRoleRouter } from './project-role/router.js'; +import { projectServiceRouter } from './project-service/router.js'; +import { projectRouter } from './project/router.js'; +import { repositoryRouter } from './repository/router.js'; +import { serviceChainRouter } from './service-chain/router.js'; +import { serviceMonitorRouter } from './service-monitor/router.js'; +import { stageRouter } from './stage/router.js'; +import { pluginConfigRouter } from './system/config/router.js'; +import { systemRouter } from './system/router.js'; +import { systemSettingsRouter } from './system/settings/router.js'; +import { userRouter } from './user/router.js'; +import { personalAccessTokenRouter } from './user/tokens/router.js'; +import { zoneRouter } from './zone/router.js'; // relax validation schema if NO_VALIDATION env var is set to true. // /!\ It can lead to security leaks !!!! -const validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' } +const validateTrue = { + responseValidation: process.env.NO_VALIDATION !== 'true', +}; export function apiRouter() { - return async (app: FastifyInstance) => { - await app.register(serverInstance.plugin(adminRoleRouter()), validateTrue) - await app.register(serverInstance.plugin(adminTokenRouter()), validateTrue) - await app.register(serverInstance.plugin(clusterRouter()), validateTrue) - await app.register(serverInstance.plugin(serviceChainRouter()), validateTrue) - await app.register(serverInstance.plugin(environmentRouter()), validateTrue) - await app.register(serverInstance.plugin(logRouter()), validateTrue) - await app.register(serverInstance.plugin(personalAccessTokenRouter()), validateTrue) - await app.register(serverInstance.plugin(projectRouter()), validateTrue) - await app.register(serverInstance.plugin(projectMemberRouter()), validateTrue) - await app.register(serverInstance.plugin(projectRoleRouter()), validateTrue) - await app.register(serverInstance.plugin(projectServiceRouter()), validateTrue) - await app.register(serverInstance.plugin(repositoryRouter()), validateTrue) - await app.register(serverInstance.plugin(serviceMonitorRouter()), validateTrue) - await app.register(serverInstance.plugin(pluginConfigRouter()), validateTrue) - await app.register(serverInstance.plugin(stageRouter()), validateTrue) - await app.register(serverInstance.plugin(systemRouter()), validateTrue) - await app.register(serverInstance.plugin(systemSettingsRouter()), validateTrue) - await app.register(serverInstance.plugin(userRouter()), validateTrue) - await app.register(serverInstance.plugin(zoneRouter()), validateTrue) - } + return async (app: FastifyInstance) => { + await app.register( + serverInstance.plugin(adminRoleRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(adminTokenRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(clusterRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(serviceChainRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(environmentRouter()), + validateTrue, + ); + await app.register(serverInstance.plugin(logRouter()), validateTrue); + await app.register( + serverInstance.plugin(personalAccessTokenRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(projectRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(projectMemberRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(projectRoleRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(projectServiceRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(repositoryRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(serviceMonitorRouter()), + validateTrue, + ); + await app.register( + serverInstance.plugin(pluginConfigRouter()), + validateTrue, + ); + await app.register(serverInstance.plugin(stageRouter()), validateTrue); + await app.register(serverInstance.plugin(systemRouter()), validateTrue); + await app.register( + serverInstance.plugin(systemSettingsRouter()), + validateTrue, + ); + await app.register(serverInstance.plugin(userRouter()), validateTrue); + await app.register(serverInstance.plugin(zoneRouter()), validateTrue); + }; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts index 9abaedc63..af71b1f20 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts @@ -1,42 +1,57 @@ -import { describe, expect, it } from 'vitest' -import { faker } from '@faker-js/faker' -import prisma from '../../__mocks__/prisma.js' -import { getLogs } from './business.ts' +import { faker } from '@faker-js/faker'; +import { describe, expect, it } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import { getLogs } from './business.ts'; describe('test log business', () => { - it('should map filter (clean logs)', async () => { - const dbLogs = [{ - data: { args: {} }, - createdAt: new Date(), - updatedAt: new Date(), - userId: null, - action: 'Action', - id: faker.string.uuid(), - }] - const query = { limit: 10, offset: 10, clean: true, projectId: undefined } - prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]) - const [_total, logs] = await getLogs(query) + it('should map filter (clean logs)', async () => { + const dbLogs = [ + { + data: { args: {} }, + createdAt: new Date(), + updatedAt: new Date(), + userId: null, + action: 'Action', + id: faker.string.uuid(), + }, + ]; + const query = { + limit: 10, + offset: 10, + clean: true, + projectId: undefined, + }; + prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]); + const [_total, logs] = await getLogs(query); - expect(logs[0]).not.haveOwnProperty('requestId') - expect(logs[0].data).not.haveOwnProperty('results') - expect(logs[0].data).not.haveOwnProperty('args') - expect(logs[0].data).not.haveOwnProperty('config') - }) + expect(logs[0]).not.haveOwnProperty('requestId'); + expect(logs[0].data).not.haveOwnProperty('results'); + expect(logs[0].data).not.haveOwnProperty('args'); + expect(logs[0].data).not.haveOwnProperty('config'); + }); - it('should not filter (admin logs)', async () => { - const dbLogs = [{ - data: { args: {} }, - createdAt: new Date(), - updatedAt: new Date(), - userId: null, - action: 'Action', - id: faker.string.uuid(), - }] - const query = { limit: 10, offset: 10, clean: false, projectId: undefined } - prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]) - const [_total, logs] = await getLogs(query) + it('should not filter (admin logs)', async () => { + const dbLogs = [ + { + data: { args: {} }, + createdAt: new Date(), + updatedAt: new Date(), + userId: null, + action: 'Action', + id: faker.string.uuid(), + }, + ]; + const query = { + limit: 10, + offset: 10, + clean: false, + projectId: undefined, + }; + prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]); + const [_total, logs] = await getLogs(query); - expect(logs[0].data).haveOwnProperty('args') - expect(logs[0].data).not.haveOwnProperty('config') - }) -}) + expect(logs[0].data).haveOwnProperty('args'); + expect(logs[0].data).not.haveOwnProperty('config'); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts index 37d9f1fb3..cb25792c5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts @@ -1,13 +1,17 @@ -import type { logContract } from '@cpn-console/shared' -import { CleanLogSchema } from '@cpn-console/shared' -import { getAllLogs } from '@old-server/resources/queries-index.js' +import type { logContract } from '@cpn-console/shared'; +import { CleanLogSchema } from '@cpn-console/shared'; +import { getAllLogs } from '@old-server/resources/queries-index.js'; -export async function getLogs({ offset, limit, projectId, clean }: typeof logContract.getLogs.query._type) { - const [total, logs] = await getAllLogs({ skip: offset, take: limit, where: { projectId } }) - return [ - total, - clean - ? logs.map(log => CleanLogSchema.parse(log)) - : logs, - ] +export async function getLogs({ + offset, + limit, + projectId, + clean, +}: typeof logContract.getLogs.query._type) { + const [total, logs] = await getAllLogs({ + skip: offset, + take: limit, + where: { projectId }, + }); + return [total, clean ? logs.map((log) => CleanLogSchema.parse(log)) : logs]; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts index 167b01369..9985206f7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts @@ -1,57 +1,69 @@ -import type { Log, Prisma, Project, User } from '@prisma/client' -import { exclude } from '@cpn-console/shared' -import prisma from '@old-server/prisma.js' +import { exclude } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; +import type { Log, Prisma, Project, User } from '@prisma/client'; // SELECT export function getAllLogsForUser(user: User, offset = 0) { - return prisma.log.findMany({ - where: { userId: user.id }, - take: 100, - skip: offset, - }) + return prisma.log.findMany({ + where: { userId: user.id }, + take: 100, + skip: offset, + }); } -export function getAllLogs({ skip = 0, take = 5, where }: Prisma.LogFindManyArgs) { - return prisma.$transaction([ - prisma.log.count({ where }), - prisma.log.findMany({ - orderBy: { - createdAt: 'desc', - }, - skip, - take, - where, - }), - ]) +export function getAllLogs({ + skip = 0, + take = 5, + where, +}: Prisma.LogFindManyArgs) { + return prisma.$transaction([ + prisma.log.count({ where }), + prisma.log.findMany({ + orderBy: { + createdAt: 'desc', + }, + skip, + take, + where, + }), + ]); } // CREATE interface AddLogsArgs { - action: Log['action'] - data: Record - userId?: User['id'] | null - requestId: string - projectId?: Project['id'] + action: Log['action']; + data: Record; + userId?: User['id'] | null; + requestId: string; + projectId?: Project['id']; } -export function addLogs({ action, data, requestId, userId = null, projectId }: AddLogsArgs) { - return prisma.log.create({ - data: { - action, - userId, - data: exclude(data, ['cluster', 'user', 'newCreds', 'apis']), - requestId, - projectId, - }, - }) +export function addLogs({ + action, + data, + requestId, + userId = null, + projectId, +}: AddLogsArgs) { + return prisma.log.create({ + data: { + action, + userId, + data: exclude(data, ['cluster', 'user', 'newCreds', 'apis']), + requestId, + projectId, + }, + }); } // TECH -export function _createLog(data: Parameters[0]['create']) { - return prisma.log.upsert({ - where: { - id: data.id, - }, - create: data, - update: data, - }) +export function _createLog( + data: Parameters[0]['create'], +) { + return prisma.log.upsert({ + where: { + id: data.id, + }, + create: data, + update: data, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts index d6c144f75..358705b0a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts @@ -1,93 +1,110 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { logContract } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetLogsMock = vi.spyOn(business, 'getLogs') +import { logContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessGetLogsMock = vi.spyOn(business, 'getLogs'); describe('test logContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('getLogs', () => { - it('should return logs for admin', async () => { - const user = getUserMockInfos(true) - const logs = [] - const total = 1 - - authUserMock.mockResolvedValueOnce(user) - businessGetLogsMock.mockResolvedValueOnce([total, logs]) - - const response = await app.inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0 }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetLogsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ total, logs }) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for non-admin, no projectId', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0 }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetLogsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - - it('should return logs for non-admin, with projectId', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 1n }) - const user = getUserMockInfos(false, undefined, projectPerms) - const projectId = faker.string.uuid() - - const logs = [] - const total = 1 - - businessGetLogsMock.mockResolvedValueOnce([total, logs]) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0, projectId, clean: false }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetLogsMock).toHaveBeenCalledWith({ clean: true, limit: 10, offset: 0, projectId }) - expect(response.statusCode).toEqual(200) - }) - - it('should not return logs for non-admin, with projectId', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - const projectId = faker.string.uuid() - - const logs = [] - const total = 1 - - businessGetLogsMock.mockResolvedValueOnce([total, logs]) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0, projectId, clean: false }) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('getLogs', () => { + it('should return logs for admin', async () => { + const user = getUserMockInfos(true); + const logs = []; + const total = 1; + + authUserMock.mockResolvedValueOnce(user); + businessGetLogsMock.mockResolvedValueOnce([total, logs]); + + const response = await app + .inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0 }) + .end(); + + expect(authUserMock).toHaveBeenCalledTimes(1); + expect(businessGetLogsMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual({ total, logs }); + expect(response.statusCode).toEqual(200); + }); + + it('should return 403 for non-admin, no projectId', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0 }) + .end(); + + expect(authUserMock).toHaveBeenCalledTimes(1); + expect(businessGetLogsMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + + it('should return logs for non-admin, with projectId', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 1n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + const projectId = faker.string.uuid(); + + const logs = []; + const total = 1; + + businessGetLogsMock.mockResolvedValueOnce([total, logs]); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0, projectId, clean: false }) + .end(); + + expect(authUserMock).toHaveBeenCalledTimes(1); + expect(businessGetLogsMock).toHaveBeenCalledWith({ + clean: true, + limit: 10, + offset: 0, + projectId, + }); + expect(response.statusCode).toEqual(200); + }); + + it('should not return logs for non-admin, with projectId', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + const projectId = faker.string.uuid(); + + const logs = []; + const total = 1; + + businessGetLogsMock.mockResolvedValueOnce([total, logs]); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0, projectId, clean: false }) + .end(); + + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts index 13affd6e7..4823b3de2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts @@ -1,32 +1,39 @@ -import type { CleanLog, Log, XOR } from '@cpn-console/shared' -import { AdminAuthorized, logContract } from '@cpn-console/shared' -import { getLogs } from './business.js' -import { serverInstance } from '@old-server/app.js' -import type { UserProfile, UserProjectProfile } from '@old-server/utils/controller.js' -import { authUser } from '@old-server/utils/controller.js' -import { Forbidden403 } from '@old-server/utils/errors.js' +import type { CleanLog, Log, XOR } from '@cpn-console/shared'; +import { AdminAuthorized, logContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import type { + UserProfile, + UserProjectProfile, +} from '@old-server/utils/controller.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { Forbidden403 } from '@old-server/utils/errors.js'; + +import { getLogs } from './business.js'; export function logRouter() { - return serverInstance.router(logContract, { - // Récupérer des logs - getLogs: async ({ request: req, query }) => { - const perms: XOR = query.projectId - ? await authUser(req, { id: query.projectId }) - : await authUser(req) + return serverInstance.router(logContract, { + // Récupérer des logs + getLogs: async ({ request: req, query }) => { + const perms: XOR = query.projectId + ? await authUser(req, { id: query.projectId }) + : await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { - if (!perms.projectPermissions) { - return new Forbidden403() - } - query.clean = true - } + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { + if (!perms.projectPermissions) { + return new Forbidden403(); + } + query.clean = true; + } - const [total, logs] = await getLogs(query) as [number, unknown[]] as [number, Array] + const [total, logs] = (await getLogs(query)) as [ + number, + unknown[], + ] as [number, Array]; - return { - status: 200, - body: { total, logs }, - } - }, - }) + return { + status: 200, + body: { total, logs }, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts index 81e0155a0..1b7a6041d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts @@ -1,60 +1,105 @@ -import type { Project, User } from '@prisma/client' -import type { XOR, projectMemberContract } from '@cpn-console/shared' -import { UserSchema } from '@cpn-console/shared' -import { logViaSession } from '../user/business.js' +import type { XOR, projectMemberContract } from '@cpn-console/shared'; +import { UserSchema } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; import { - addLogs, - deleteMember, - listMembers as listMembersQuery, - upsertMember, -} from '@old-server/resources/queries-index.js' -import prisma from '@old-server/prisma.js' -import { BadRequest400, NotFound404 } from '@old-server/utils/errors.js' -import { hook } from '@old-server/utils/hook-wrapper.js' - -export const listMembers = async (projectId: Project['id']) => listMembersQuery(projectId) - -export async function addMember(projectId: Project['id'], user: XOR<{ userId: string }, { email: string }>, requestorId: User['id'], requestId: string, projectOwnerId: Project['ownerId']) { - let userInDb: User | undefined | null - - if (user.userId) { - userInDb = await prisma.user.findUnique({ where: { id: user.userId, type: 'human' } }) - } else if (user.email) { - userInDb = await prisma.user.findUnique({ where: { email: user.email, type: 'human' } }) - } else { - return new BadRequest400('Veuillez spécifiez au moins un userId ou un email') - } - if (userInDb) { - if (userInDb.id === projectOwnerId) return new BadRequest400('Le owner ne peut pas être ajouté à cette liste') - } else if (user.email) { - const hookReply = await hook.user.retrieveUserByEmail(user.email) - await addLogs({ action: 'Retrieve User By Email', data: hookReply, userId: requestorId, requestId }) - if (hookReply.failed) { - throw new BadRequest400('Echec de la recherche auprès des services externes') + addLogs, + deleteMember, + listMembers as listMembersQuery, + upsertMember, +} from '@old-server/resources/queries-index.js'; +import { BadRequest400, NotFound404 } from '@old-server/utils/errors.js'; +import { hook } from '@old-server/utils/hook-wrapper.js'; +import type { Project, User } from '@prisma/client'; + +import { logViaSession } from '../user/business.js'; + +export const listMembers = async (projectId: Project['id']) => + listMembersQuery(projectId); + +export async function addMember( + projectId: Project['id'], + user: XOR<{ userId: string }, { email: string }>, + requestorId: User['id'], + requestId: string, + projectOwnerId: Project['ownerId'], +) { + let userInDb: User | undefined | null; + + if (user.userId) { + userInDb = await prisma.user.findUnique({ + where: { id: user.userId, type: 'human' }, + }); + } else if (user.email) { + userInDb = await prisma.user.findUnique({ + where: { email: user.email, type: 'human' }, + }); + } else { + return new BadRequest400( + 'Veuillez spécifiez au moins un userId ou un email', + ); } + if (userInDb) { + if (userInDb.id === projectOwnerId) + return new BadRequest400( + 'Le owner ne peut pas être ajouté à cette liste', + ); + } else if (user.email) { + const hookReply = await hook.user.retrieveUserByEmail(user.email); + await addLogs({ + action: 'Retrieve User By Email', + data: hookReply, + userId: requestorId, + requestId, + }); + if (hookReply.failed) { + throw new BadRequest400( + 'Echec de la recherche auprès des services externes', + ); + } - const retrievedUser = hookReply.results.keycloak?.user - if (!retrievedUser) return new BadRequest400('Utilisateur introuvable') - const userValidated = UserSchema.pick({ email: true, firstName: true, lastName: true, id: true }).safeParse(retrievedUser) - if (!userValidated.success) return new BadRequest400('L\'utilisateur trouvé ne remplit pas les conditions de vérification') - const logResults = await logViaSession({ ...userValidated.data, groups: [] }) - userInDb = logResults.user - } else { - return new NotFound404() - } - - await upsertMember({ projectId, userId: userInDb.id, roleIds: [] }) - return listMembers(projectId) + const retrievedUser = hookReply.results.keycloak?.user; + if (!retrievedUser) return new BadRequest400('Utilisateur introuvable'); + const userValidated = UserSchema.pick({ + email: true, + firstName: true, + lastName: true, + id: true, + }).safeParse(retrievedUser); + if (!userValidated.success) + return new BadRequest400( + "L'utilisateur trouvé ne remplit pas les conditions de vérification", + ); + const logResults = await logViaSession({ + ...userValidated.data, + groups: [], + }); + userInDb = logResults.user; + } else { + return new NotFound404(); + } + + await upsertMember({ projectId, userId: userInDb.id, roleIds: [] }); + return listMembers(projectId); } -export async function patchMembers(projectId: Project['id'], members: typeof projectMemberContract.patchMembers.body._type) { - for (const member of members) { - await upsertMember({ projectId, userId: member.userId, roleIds: member.roles }) - } - return listMembers(projectId) +export async function patchMembers( + projectId: Project['id'], + members: typeof projectMemberContract.patchMembers.body._type, +) { + for (const member of members) { + await upsertMember({ + projectId, + userId: member.userId, + roleIds: member.roles, + }); + } + return listMembers(projectId); } -export async function removeMember(projectId: Project['id'], userId: User['id']) { - await deleteMember({ projectId, userId }) - return listMembers(projectId) +export async function removeMember( + projectId: Project['id'], + userId: User['id'], +) { + await deleteMember({ projectId, userId }); + return listMembers(projectId); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts index 0161cc776..4400035bc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts @@ -1,33 +1,34 @@ -import type { - Prisma, +import prisma from '@old-server/prisma.js'; +import type { Prisma, Project } from '@prisma/client'; - Project, -} from '@prisma/client' - -import prisma from '@old-server/prisma.js' - -export const listMembers = (projectId: Project['id']) => prisma.projectMembers.findMany({ where: { projectId }, include: { user: true } }) +export const listMembers = (projectId: Project['id']) => + prisma.projectMembers.findMany({ + where: { projectId }, + include: { user: true }, + }); export function upsertMember(data: Prisma.ProjectMembersUncheckedCreateInput) { - return prisma.projectMembers.upsert({ - where: { - projectId_userId: { - userId: data.userId, - projectId: data.projectId, - }, - }, - create: data, - update: { - roleIds: data.roleIds, - }, - include: { user: true }, - }) + return prisma.projectMembers.upsert({ + where: { + projectId_userId: { + userId: data.userId, + projectId: data.projectId, + }, + }, + create: data, + update: { + roleIds: data.roleIds, + }, + include: { user: true }, + }); } -export function deleteMember(data: Prisma.ProjectMembersWhereUniqueInput['projectId_userId']) { - return prisma.projectMembers.delete({ - where: { - projectId_userId: data, - }, - }) +export function deleteMember( + data: Prisma.ProjectMembersWhereUniqueInput['projectId_userId'], +) { + return prisma.projectMembers.delete({ + where: { + projectId_userId: data, + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts index 2cb64f749..5eb162fe7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts @@ -1,294 +1,456 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Member } from '@cpn-console/shared' -import { PROJECT_PERMS, projectMemberContract } from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' -import { BadRequest400 } from '../../utils/errors.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMembersMock = vi.spyOn(business, 'listMembers') -const businessAddMemberMock = vi.spyOn(business, 'addMember') -const businessPatchMembersMock = vi.spyOn(business, 'patchMembers') -const businessRemoveMemberMock = vi.spyOn(business, 'removeMember') +import type { Member } from '@cpn-console/shared'; +import { PROJECT_PERMS, projectMemberContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessListMembersMock = vi.spyOn(business, 'listMembers'); +const businessAddMemberMock = vi.spyOn(business, 'addMember'); +const businessPatchMembersMock = vi.spyOn(business, 'patchMembers'); +const businessRemoveMemberMock = vi.spyOn(business, 'removeMember'); describe('projectMemberRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const projectId = faker.string.uuid() - const userId = faker.string.uuid() - - describe('listMembers', () => { - it('should return members for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessListMembersMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectMemberContract.listMembers.path.replace(':projectId', projectId)) - .end() - - expect(businessListMembersMock).toHaveBeenCalledWith(projectId) - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectMemberContract.listMembers.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - }) - - describe('addMember', () => { - const memberData: Partial = { - userId: faker.string.uuid(), - } - - it('should add member for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - const newMember = { - ...memberData, - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - roleIds: [], - } - - businessAddMemberMock.mockResolvedValueOnce([newMember]) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.json()).toEqual([newMember]) - expect(response.statusCode).toEqual(201) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - businessAddMemberMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) - - describe('patchMembers', () => { - const patchData = [{ userId: faker.string.uuid(), roles: [] }] - - it('should patch members for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessPatchMembersMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) - - describe('removeMember', () => { - it('should remove member for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessRemoveMemberMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should be able leave a project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessRemoveMemberMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + const projectId = faker.string.uuid(); + const userId = faker.string.uuid(); + + describe('listMembers', () => { + it('should return members for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessListMembersMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .get( + projectMemberContract.listMembers.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(businessListMembersMock).toHaveBeenCalledWith(projectId); + expect(response.statusCode).toEqual(200); + expect(response.json()).toEqual([]); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + projectMemberContract.listMembers.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(404); + }); + }); + + describe('addMember', () => { + const memberData: Partial = { + userId: faker.string.uuid(), + }; + + it('should add member for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + const newMember = { + ...memberData, + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + roleIds: [], + }; + + businessAddMemberMock.mockResolvedValueOnce([newMember]); + + const response = await app + .inject() + .post( + projectMemberContract.addMember.path.replace( + ':projectId', + projectId, + ), + ) + .body(memberData) + .end(); + + expect(response.json()).toEqual([newMember]); + expect(response.statusCode).toEqual(201); + }); + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + businessAddMemberMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + + const response = await app + .inject() + .post( + projectMemberContract.addMember.path.replace( + ':projectId', + projectId, + ), + ) + .body(memberData) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectMemberContract.addMember.path.replace( + ':projectId', + projectId, + ), + ) + .body(memberData) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectMemberContract.addMember.path.replace( + ':projectId', + projectId, + ), + ) + .body(memberData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectMemberContract.addMember.path.replace( + ':projectId', + projectId, + ), + ) + .body(memberData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + }); + + describe('patchMembers', () => { + const patchData = [{ userId: faker.string.uuid(), roles: [] }]; + + it('should patch members for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessPatchMembersMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .patch( + projectMemberContract.patchMembers.path.replace( + ':projectId', + projectId, + ), + ) + .body(patchData) + .end(); + + expect(response.json()).toEqual([]); + expect(response.statusCode).toEqual(200); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch( + projectMemberContract.patchMembers.path.replace( + ':projectId', + projectId, + ), + ) + .body(patchData) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch( + projectMemberContract.patchMembers.path.replace( + ':projectId', + projectId, + ), + ) + .body(patchData) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch( + projectMemberContract.patchMembers.path.replace( + ':projectId', + projectId, + ), + ) + .body(patchData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch( + projectMemberContract.patchMembers.path.replace( + ':projectId', + projectId, + ), + ) + .body(patchData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + }); + + describe('removeMember', () => { + it('should remove member for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessRemoveMemberMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .delete( + projectMemberContract.removeMember.path + .replace(':projectId', projectId) + .replace(':userId', userId), + ) + .end(); + + expect(response.json()).toEqual([]); + expect(response.statusCode).toEqual(200); + }); + + it('should be able leave a project', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessRemoveMemberMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .delete( + projectMemberContract.removeMember.path + .replace(':projectId', projectId) + .replace(':userId', userId), + ) + .end(); + + expect(response.json()).toEqual([]); + expect(response.statusCode).toEqual(200); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + projectMemberContract.removeMember.path + .replace(':projectId', projectId) + .replace(':userId', userId), + ) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + projectMemberContract.removeMember.path + .replace(':projectId', projectId) + .replace(':userId', userId), + ) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + projectMemberContract.removeMember.path + .replace(':projectId', projectId) + .replace(':userId', userId), + ) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + projectMemberContract.removeMember.path + .replace(':projectId', projectId) + .replace(':userId', userId), + ) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts index 905e99f8c..213be32d6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts @@ -1,82 +1,125 @@ -import { AdminAuthorized, ProjectAuthorized, projectMemberContract } from '@cpn-console/shared' import { - addMember, - listMembers, - patchMembers, - removeMember, -} from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors.js' + AdminAuthorized, + ProjectAuthorized, + projectMemberContract, +} from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { + ErrorResType, + Forbidden403, + NotFound404, + Unauthorized401, +} from '@old-server/utils/errors.js'; + +import { + addMember, + listMembers, + patchMembers, + removeMember, +} from './business.js'; export function projectMemberRouter() { - return serverInstance.router(projectMemberContract, { - listMembers: async ({ request: req, params }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - const body = await listMembers(projectId) - - return { - status: 200, - body, - } - }, - - addMember: async ({ request: req, params, body }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await addMember(projectId, body, perms.user.id, req.id, perms.projectOwnerId) - if (resBody instanceof ErrorResType) return resBody - - return { - status: 201, - body: resBody, - } - }, - - patchMembers: async ({ request: req, params, body }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await patchMembers(projectId, body) - - return { - status: 200, - body: resBody, - } - }, - - removeMember: async ({ request: req, params }) => { - const { projectId, userId } = params - const perms = await authUser(req, { id: projectId }) - - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - if (!ProjectAuthorized.ManageMembers(perms) && userId !== perms.user?.id) return new Forbidden403() - - const resBody = await removeMember(projectId, params.userId) - - return { - status: 200, - body: resBody, - } - }, - }) + return serverInstance.router(projectMemberContract, { + listMembers: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const body = await listMembers(projectId); + + return { + status: 200, + body, + }; + }, + + addMember: async ({ request: req, params, body }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageMembers(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await addMember( + projectId, + body, + perms.user.id, + req.id, + perms.projectOwnerId, + ); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 201, + body: resBody, + }; + }, + + patchMembers: async ({ request: req, params, body }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageMembers(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await patchMembers(projectId, body); + + return { + status: 200, + body: resBody, + }; + }, + + removeMember: async ({ request: req, params }) => { + const { projectId, userId } = params; + const perms = await authUser(req, { id: projectId }); + + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + if ( + !ProjectAuthorized.ManageMembers(perms) && + userId !== perms.user?.id + ) + return new Forbidden403(); + + const resBody = await removeMember(projectId, params.userId); + + return { + status: 200, + body: resBody, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts index cdbaa3fd1..cbf7086c3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts @@ -1,195 +1,236 @@ -import { faker } from '@faker-js/faker' -import { describe, expect, it } from 'vitest' -import type { ProjectMembers, ProjectRole, User } from '@prisma/client' -import prisma from '../../__mocks__/prisma.js' -import { BadRequest400 } from '../../utils/errors.ts' -import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business.ts' - -const projectId = faker.string.uuid() +import { faker } from '@faker-js/faker'; +import type { ProjectMembers, ProjectRole, User } from '@prisma/client'; +import { describe, expect, it } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import { BadRequest400 } from '../../utils/errors.ts'; +import { + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from './business.ts'; + +const projectId = faker.string.uuid(); describe('test project-role business', () => { - describe('listRoles', () => { - it('should stringify bigint', async () => { - const partialRole: Partial = { - permissions: 4n, - } - - prisma.projectRole.findMany.mockResolvedValueOnce([partialRole]) - const response = await listRoles(projectId) - expect(response).toEqual([{ permissions: '4' }]) - }) - }) - - describe('createRole', () => { - it('should create role with incremented position when position 0 is the highest', async () => { - const dbRole: Partial = { - projectId, - permissions: 4n, - position: 0, - } - - prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole) - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.projectRole.create.mockResolvedValue(null) - await createRole(projectId, { name: 'test', permissions: '4' }) - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 1, projectId } }) - }) - - it('should create role with incremented position with bigger position', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - } - - prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole) - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.projectRole.create.mockResolvedValue(null) - await createRole(projectId, { name: 'test', permissions: '4' }) - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 51, projectId } }) - }) - - it('should create role with incremented position with no role in db', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - } - - prisma.projectRole.findFirst.mockResolvedValueOnce(undefined) - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.projectRole.create.mockResolvedValue(null) - await createRole(projectId, { name: 'test', permissions: '4' }) - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 0, projectId } }) - }) - }) - - describe('deleteRole', () => { - const roleId = faker.string.uuid() - it('should delete role and remove id from concerned users', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - id: faker.string.uuid(), - } - const members = [{ - userId: faker.string.uuid(), - projectId, - roleIds: [roleId], - }, { - userId: faker.string.uuid(), - projectId, - roleIds: [roleId, faker.string.uuid()], - }] as const satisfies Partial[] - - prisma.projectMembers.findMany.mockResolvedValueOnce(members) - prisma.projectRole.findMany.mockResolvedValueOnce([]) - prisma.projectRole.delete.mockResolvedValue(dbRole) - await deleteRole(roleId) - - expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(1, { where: expect.any(Object), data: { roleIds: { set: [] } } }) - expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(2, { where: expect.any(Object), data: { roleIds: { set: [members[1].roleIds[1]] } } }) - expect(prisma.projectRole.delete).toHaveBeenCalledWith({ where: { id: roleId } }) - }) - }) - describe.skip('countRolesMembers', () => { - it('should return aggregated role member counts', async () => { - const partialRoles = [{ - id: faker.string.uuid(), - }, { - id: faker.string.uuid(), - }] as const satisfies Partial[] - - const users = [{ - projectRoleIds: [partialRoles[0].id, partialRoles[1].id], - }, { - projectRoleIds: [partialRoles[1].id], - }] as const satisfies Partial[] - prisma.projectRole.findMany.mockResolvedValue(partialRoles) - prisma.user.findMany.mockResolvedValue(users) - - const response = await countRolesMembers() - - expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 }) - }) - }) - describe('patchRoles', () => { - const dbRoles: ProjectRole[] = [{ - id: faker.string.uuid(), - name: faker.company.name(), - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 0, - projectId, - }, { - id: faker.string.uuid(), - name: faker.company.name(), - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 1, - projectId, - }] - - it('should do nothing', async () => { - prisma.projectRole.findMany.mockResolvedValue([]) - await patchRoles(projectId, []) - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) - }) - - it('should return 400 if incoherent positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 1 }, - ] - prisma.projectRole.findMany.mockResolvedValue(dbRoles) - - const response = await patchRoles(projectId, updateRoles) - - expect(response).instanceOf(BadRequest400) - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) - }) - - it('should return 400 if incoherent positions (missing)', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, position: 1 }, - ] - prisma.projectRole.findMany.mockResolvedValue(dbRoles) - - const response = await patchRoles(projectId, updateRoles) - - expect(response).instanceOf(BadRequest400) - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) - }) - - it('should update positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 0 }, - ] - prisma.projectRole.findMany.mockResolvedValue(dbRoles) - - await patchRoles(projectId, updateRoles) - - expect(prisma.projectRole.update).toHaveBeenCalledTimes(2) - }) - - it('should update permissions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, permissions: '0' }, - ] - prisma.projectRole.findMany.mockResolvedValue(dbRoles) - - await patchRoles(projectId, updateRoles) - - expect(prisma.projectRole.update).toHaveBeenCalledTimes(1) - expect(prisma.projectRole.update).toHaveBeenCalledWith({ - data: { - name: dbRoles[1].name, - permissions: 0n, - position: 1, - }, - where: { - id: dbRoles[1].id, - }, - }) - }) - }) -}) + describe('listRoles', () => { + it('should stringify bigint', async () => { + const partialRole: Partial = { + permissions: 4n, + }; + + prisma.projectRole.findMany.mockResolvedValueOnce([partialRole]); + const response = await listRoles(projectId); + expect(response).toEqual([{ permissions: '4' }]); + }); + }); + + describe('createRole', () => { + it('should create role with incremented position when position 0 is the highest', async () => { + const dbRole: Partial = { + projectId, + permissions: 4n, + position: 0, + }; + + prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole); + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]); + prisma.projectRole.create.mockResolvedValue(null); + await createRole(projectId, { name: 'test', permissions: '4' }); + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ + data: { name: 'test', permissions: 4n, position: 1, projectId }, + }); + }); + + it('should create role with incremented position with bigger position', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + }; + + prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole); + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]); + prisma.projectRole.create.mockResolvedValue(null); + await createRole(projectId, { name: 'test', permissions: '4' }); + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ + data: { + name: 'test', + permissions: 4n, + position: 51, + projectId, + }, + }); + }); + + it('should create role with incremented position with no role in db', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + }; + + prisma.projectRole.findFirst.mockResolvedValueOnce(undefined); + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]); + prisma.projectRole.create.mockResolvedValue(null); + await createRole(projectId, { name: 'test', permissions: '4' }); + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ + data: { name: 'test', permissions: 4n, position: 0, projectId }, + }); + }); + }); + + describe('deleteRole', () => { + const roleId = faker.string.uuid(); + it('should delete role and remove id from concerned users', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + id: faker.string.uuid(), + }; + const members = [ + { + userId: faker.string.uuid(), + projectId, + roleIds: [roleId], + }, + { + userId: faker.string.uuid(), + projectId, + roleIds: [roleId, faker.string.uuid()], + }, + ] as const satisfies Partial[]; + + prisma.projectMembers.findMany.mockResolvedValueOnce(members); + prisma.projectRole.findMany.mockResolvedValueOnce([]); + prisma.projectRole.delete.mockResolvedValue(dbRole); + await deleteRole(roleId); + + expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(1, { + where: expect.any(Object), + data: { roleIds: { set: [] } }, + }); + expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(2, { + where: expect.any(Object), + data: { roleIds: { set: [members[1].roleIds[1]] } }, + }); + expect(prisma.projectRole.delete).toHaveBeenCalledWith({ + where: { id: roleId }, + }); + }); + }); + describe.skip('countRolesMembers', () => { + it('should return aggregated role member counts', async () => { + const partialRoles = [ + { + id: faker.string.uuid(), + }, + { + id: faker.string.uuid(), + }, + ] as const satisfies Partial[]; + + const users = [ + { + projectRoleIds: [partialRoles[0].id, partialRoles[1].id], + }, + { + projectRoleIds: [partialRoles[1].id], + }, + ] as const satisfies Partial[]; + prisma.projectRole.findMany.mockResolvedValue(partialRoles); + prisma.user.findMany.mockResolvedValue(users); + + const response = await countRolesMembers(); + + expect(response).toEqual({ + [partialRoles[0].id]: 1, + [partialRoles[1].id]: 2, + }); + }); + }); + describe('patchRoles', () => { + const dbRoles: ProjectRole[] = [ + { + id: faker.string.uuid(), + name: faker.company.name(), + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 0, + projectId, + }, + { + id: faker.string.uuid(), + name: faker.company.name(), + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 1, + projectId, + }, + ]; + + it('should do nothing', async () => { + prisma.projectRole.findMany.mockResolvedValue([]); + await patchRoles(projectId, []); + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0); + }); + + it('should return 400 if incoherent positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 1 }, + ]; + prisma.projectRole.findMany.mockResolvedValue(dbRoles); + + const response = await patchRoles(projectId, updateRoles); + + expect(response).instanceOf(BadRequest400); + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0); + }); + + it('should return 400 if incoherent positions (missing)', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, position: 1 }, + ]; + prisma.projectRole.findMany.mockResolvedValue(dbRoles); + + const response = await patchRoles(projectId, updateRoles); + + expect(response).instanceOf(BadRequest400); + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0); + }); + + it('should update positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 0 }, + ]; + prisma.projectRole.findMany.mockResolvedValue(dbRoles); + + await patchRoles(projectId, updateRoles); + + expect(prisma.projectRole.update).toHaveBeenCalledTimes(2); + }); + + it('should update permissions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, permissions: '0' }, + ]; + prisma.projectRole.findMany.mockResolvedValue(dbRoles); + + await patchRoles(projectId, updateRoles); + + expect(prisma.projectRole.update).toHaveBeenCalledTimes(1); + expect(prisma.projectRole.update).toHaveBeenCalledWith({ + data: { + name: dbRoles[1].name, + permissions: 0n, + position: 1, + }, + where: { + id: dbRoles[1].id, + }, + }); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts index 8631d12a2..11aa5791b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts @@ -1,77 +1,103 @@ -import type { projectRoleContract } from '@cpn-console/shared' -import type { Project, ProjectRole } from '@prisma/client' +import type { projectRoleContract } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; import { - deleteRole as deleteRoleQuery, - listMembers, - listRoles as listRolesQuery, - updateRole, -} from '@old-server/resources/queries-index.js' -import { BadRequest400 } from '@old-server/utils/errors.js' -import prisma from '@old-server/prisma.js' + deleteRole as deleteRoleQuery, + listMembers, + listRoles as listRolesQuery, + updateRole, +} from '@old-server/resources/queries-index.js'; +import { BadRequest400 } from '@old-server/utils/errors.js'; +import type { Project, ProjectRole } from '@prisma/client'; export async function listRoles(projectId: Project['id']) { - return listRolesQuery(projectId) - .then(roles => roles.map(role => ({ ...role, permissions: role.permissions.toString() }))) + return listRolesQuery(projectId).then((roles) => + roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + ); } -export async function patchRoles(projectId: Project['id'], roles: typeof projectRoleContract.patchProjectRoles.body._type) { - const dbRoles = await listRoles(projectId) - const positionsAvailable: number[] = [] +export async function patchRoles( + projectId: Project['id'], + roles: typeof projectRoleContract.patchProjectRoles.body._type, +) { + const dbRoles = await listRoles(projectId); + const positionsAvailable: number[] = []; - const updatedRoles = dbRoles - .filter(dbRole => roles.find(role => role.id === dbRole.id)) // filter non concerned dbRoles - .map((dbRole) => { - const matchingRole = roles.find(role => role.id === dbRole.id) - if (typeof matchingRole?.position !== 'undefined' && !positionsAvailable.includes(matchingRole.position)) { - positionsAvailable.push(matchingRole.position) - } - return { - id: matchingRole?.id ?? dbRole.id, - name: matchingRole?.name ?? dbRole.name, - permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : BigInt(dbRole.permissions), - position: matchingRole?.position ?? dbRole.position, - } - }) - if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes') - for (const { id, ...role } of updatedRoles) { - await updateRole(id, role) - } + const updatedRoles = dbRoles + .filter((dbRole) => roles.find((role) => role.id === dbRole.id)) // filter non concerned dbRoles + .map((dbRole) => { + const matchingRole = roles.find((role) => role.id === dbRole.id); + if ( + typeof matchingRole?.position !== 'undefined' && + !positionsAvailable.includes(matchingRole.position) + ) { + positionsAvailable.push(matchingRole.position); + } + return { + id: matchingRole?.id ?? dbRole.id, + name: matchingRole?.name ?? dbRole.name, + permissions: matchingRole?.permissions + ? BigInt(matchingRole?.permissions) + : BigInt(dbRole.permissions), + position: matchingRole?.position ?? dbRole.position, + }; + }); + if ( + positionsAvailable.length && + positionsAvailable.length !== dbRoles.length + ) + return new BadRequest400( + 'Les numéros de position des rôles sont incohérentes', + ); + for (const { id, ...role } of updatedRoles) { + await updateRole(id, role); + } - return listRoles(projectId) + return listRoles(projectId); } -export async function createRole(projectId: Project['id'], role: typeof projectRoleContract.createProjectRole.body._type) { - const dbMaxPosRole = (await prisma.projectRole.findFirst({ - where: { projectId }, - orderBy: { position: 'desc' }, - select: { position: true }, - }))?.position ?? -1 +export async function createRole( + projectId: Project['id'], + role: typeof projectRoleContract.createProjectRole.body._type, +) { + const dbMaxPosRole = + ( + await prisma.projectRole.findFirst({ + where: { projectId }, + orderBy: { position: 'desc' }, + select: { position: true }, + }) + )?.position ?? -1; - await prisma.projectRole.create({ - data: { - ...role, - projectId, - position: dbMaxPosRole + 1, - permissions: BigInt(role.permissions), - }, - }) + await prisma.projectRole.create({ + data: { + ...role, + projectId, + position: dbMaxPosRole + 1, + permissions: BigInt(role.permissions), + }, + }); - return listRoles(projectId) + return listRoles(projectId); } export async function countRolesMembers(projectId: Project['id']) { - const roles = await listRoles(projectId) - const members = await listMembers(projectId) - const rolesCounts: Record = Object.fromEntries(roles.map(role => [role.id, 0])) // {role uuid: 0} - for (const { roleIds } of members) { - for (const roleId of roleIds) { - rolesCounts[roleId]++ + const roles = await listRoles(projectId); + const members = await listMembers(projectId); + const rolesCounts: Record = Object.fromEntries( + roles.map((role) => [role.id, 0]), + ); // {role uuid: 0} + for (const { roleIds } of members) { + for (const roleId of roleIds) { + rolesCounts[roleId]++; + } } - } - return rolesCounts + return rolesCounts; } export async function deleteRole(roleId: Project['id']) { - await deleteRoleQuery(roleId) - return null + await deleteRoleQuery(roleId); + return null; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts index 25690dff2..1de745cd4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts @@ -1,54 +1,63 @@ -import type { - Prisma, - Project, +import prisma from '@old-server/prisma.js'; +import type { Prisma, Project, ProjectRole } from '@prisma/client'; - ProjectRole, -} from '@prisma/client' +export const listRoles = (projectId: Project['id']) => + prisma.projectRole.findMany({ + where: { projectId }, + orderBy: { position: 'asc' }, + }); -import prisma from '@old-server/prisma.js' - -export const listRoles = (projectId: Project['id']) => prisma.projectRole.findMany({ where: { projectId }, orderBy: { position: 'asc' } }) - -export function createRole(data: Pick) { - return prisma.projectRole.create({ - data: { - name: data.name, - permissions: 0n, - position: data.position, - projectId: data.projectId, - }, - }) +export function createRole( + data: Pick< + Prisma.ProjectRoleUncheckedCreateInput, + 'permissions' | 'name' | 'position' | 'projectId' + >, +) { + return prisma.projectRole.create({ + data: { + name: data.name, + permissions: 0n, + position: data.position, + projectId: data.projectId, + }, + }); } -export function updateRole(id: ProjectRole['id'], data: Pick) { - return prisma.projectRole.update({ - where: { id }, - data, - }) +export function updateRole( + id: ProjectRole['id'], + data: Pick< + Prisma.ProjectRoleUncheckedUpdateInput, + 'permissions' | 'name' | 'position' | 'id' + >, +) { + return prisma.projectRole.update({ + where: { id }, + data, + }); } export async function deleteRole(id: ProjectRole['id']) { - const role = await prisma.projectRole.delete({ - where: { - id, - }, - }) - const attachedMembers = await prisma.projectMembers.findMany({ - where: { projectId: role.projectId, roleIds: { has: id } }, - }) - for (const member of attachedMembers) { - await prisma.projectMembers.update({ - where: { - projectId_userId: { - projectId: role.projectId, - userId: member.userId, - }, - }, - data: { - roleIds: { - set: member.roleIds.filter(roleId => roleId !== id), + const role = await prisma.projectRole.delete({ + where: { + id, }, - }, - }) - } + }); + const attachedMembers = await prisma.projectMembers.findMany({ + where: { projectId: role.projectId, roleIds: { has: id } }, + }); + for (const member of attachedMembers) { + await prisma.projectMembers.update({ + where: { + projectId_userId: { + projectId: role.projectId, + userId: member.userId, + }, + }, + data: { + roleIds: { + set: member.roleIds.filter((roleId) => roleId !== id), + }, + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts index f6a0539c7..59a0bbf05 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts @@ -1,316 +1,495 @@ -import { faker } from '@faker-js/faker' -import { PROJECT_PERMS, projectRoleContract } from '@cpn-console/shared' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' -import { BadRequest400 } from '../../utils/errors.js' -import * as business from './business.js' - -vi.mock('./business.js') -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) - -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessCreateRoleMock = vi.spyOn(business, 'createRole') -const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole') -const businessListRolesMock = vi.spyOn(business, 'listRoles') -const businessPatchRolesMock = vi.spyOn(business, 'patchRoles') -const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers') +import { PROJECT_PERMS, projectRoleContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock('./business.js'); +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); + +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessCreateRoleMock = vi.spyOn(business, 'createRole'); +const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole'); +const businessListRolesMock = vi.spyOn(business, 'listRoles'); +const businessPatchRolesMock = vi.spyOn(business, 'patchRoles'); +const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers'); describe('tests projectRoleContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const projectId = faker.string.uuid() - const roleId = faker.string.uuid() - - describe('listProjectRoles', () => { - it('should return roles for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessListRolesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectRoleContract.listProjectRoles.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectRoleContract.listProjectRoles.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - }) - - describe('createProjectRole', () => { - it('should create role for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(201) - }) - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) - - describe('patchProjectRoles', () => { - it('should patch roles for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessPatchRolesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessPatchRolesMock.mockResolvedValue(new BadRequest400('une erreur')) - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('projectRoleMemberCounts', () => { - it('should return member counts for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCountRolesMembersMock.mockResolvedValueOnce({}) - - const response = await app.inject() - .get(projectRoleContract.projectRoleMemberCounts.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual({}) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectRoleContract.projectRoleMemberCounts.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - }) - - describe('deleteProjectRole', () => { - it('should delete role for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteRoleMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + const projectId = faker.string.uuid(); + const roleId = faker.string.uuid(); + + describe('listProjectRoles', () => { + it('should return roles for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.SEE_SECRETS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessListRolesMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .get( + projectRoleContract.listProjectRoles.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(200); + expect(response.json()).toEqual([]); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + projectRoleContract.listProjectRoles.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(404); + }); + }); + + describe('createProjectRole', () => { + it('should create role for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCreateRoleMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .post( + projectRoleContract.createProjectRole.path.replace( + ':projectId', + projectId, + ), + ) + .body({ name: 'nouveau rôle' }) + .end(); + + expect(response.json()).toEqual([]); + expect(response.statusCode).toEqual(201); + }); + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectRoleContract.createProjectRole.path.replace( + ':projectId', + projectId, + ), + ) + .body({ name: 'nouveau rôle' }) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.SEE_SECRETS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectRoleContract.createProjectRole.path.replace( + ':projectId', + projectId, + ), + ) + .body({ name: 'nouveau rôle' }) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectRoleContract.createProjectRole.path.replace( + ':projectId', + projectId, + ), + ) + .body({ name: 'nouveau rôle' }) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectRoleContract.createProjectRole.path.replace( + ':projectId', + projectId, + ), + ) + .body({ name: 'nouveau rôle' }) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + }); + + describe('patchProjectRoles', () => { + it('should patch roles for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessPatchRolesMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .patch( + projectRoleContract.patchProjectRoles.path.replace( + ':projectId', + projectId, + ), + ) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end(); + + expect(response.json()).toEqual([]); + expect(response.statusCode).toEqual(200); + }); + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch( + projectRoleContract.patchProjectRoles.path.replace( + ':projectId', + projectId, + ), + ) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.SEE_SECRETS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch( + projectRoleContract.patchProjectRoles.path.replace( + ':projectId', + projectId, + ), + ) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch( + projectRoleContract.patchProjectRoles.path.replace( + ':projectId', + projectId, + ), + ) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch( + projectRoleContract.patchProjectRoles.path.replace( + ':projectId', + projectId, + ), + ) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessPatchRolesMock.mockResolvedValue( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .patch( + projectRoleContract.patchProjectRoles.path.replace( + ':projectId', + projectId, + ), + ) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end(); + + expect(response.statusCode).toEqual(400); + }); + }); + + describe('projectRoleMemberCounts', () => { + it('should return member counts for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.SEE_SECRETS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCountRolesMembersMock.mockResolvedValueOnce({}); + + const response = await app + .inject() + .get( + projectRoleContract.projectRoleMemberCounts.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(200); + expect(response.json()).toEqual({}); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + projectRoleContract.projectRoleMemberCounts.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(404); + }); + }); + + describe('deleteProjectRole', () => { + it('should delete role for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteRoleMock.mockResolvedValueOnce(null); + const response = await app + .inject() + .delete( + projectRoleContract.deleteProjectRole.path + .replace(':projectId', projectId) + .replace(':roleId', roleId), + ) + .end(); + + expect(response.statusCode).toEqual(204); + }); + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCreateRoleMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .delete( + projectRoleContract.deleteProjectRole.path + .replace(':projectId', projectId) + .replace(':roleId', roleId), + ) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.SEE_SECRETS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCreateRoleMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .delete( + projectRoleContract.deleteProjectRole.path + .replace(':projectId', projectId) + .replace(':roleId', roleId), + ) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCreateRoleMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .delete( + projectRoleContract.deleteProjectRole.path + .replace(':projectId', projectId) + .replace(':roleId', roleId), + ) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_ROLES, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCreateRoleMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .delete( + projectRoleContract.deleteProjectRole.path + .replace(':projectId', projectId) + .replace(':roleId', roleId), + ) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts index d11db92bf..729ec6956 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts @@ -1,90 +1,131 @@ -import { AdminAuthorized, ProjectAuthorized, projectRoleContract } from '@cpn-console/shared' import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403, NotFound404 } from '@old-server/utils/errors.js' + AdminAuthorized, + ProjectAuthorized, + projectRoleContract, +} from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { + ErrorResType, + Forbidden403, + NotFound404, +} from '@old-server/utils/errors.js'; + +import { + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from './business.js'; export function projectRoleRouter() { - return serverInstance.router(projectRoleContract, { - // Récupérer des projets - listProjectRoles: async ({ request: req, params }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - const body = await listRoles(projectId) - - return { - status: 200, - body, - } - }, - - createProjectRole: async ({ request: req, params: { projectId }, body }) => { - const perms = await authUser(req, { id: projectId }) - - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await createRole(projectId, body) - - return { - status: 201, - body: resBody, - } - }, - - patchProjectRoles: async ({ request: req, params: { projectId }, body }) => { - const perms = await authUser(req, { id: projectId }) - - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await patchRoles(projectId, body) - if (resBody instanceof ErrorResType) return resBody - - return { - status: 200, - body: resBody, - } - }, - - projectRoleMemberCounts: async ({ request: req, params }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - const resBody = await countRolesMembers(projectId) - - return { - status: 200, - body: resBody, - } - }, - - deleteProjectRole: async ({ request: req, params: { projectId, roleId } }) => { - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await deleteRole(roleId) - - return { - status: 204, - body: resBody, - } - }, - }) + return serverInstance.router(projectRoleContract, { + // Récupérer des projets + listProjectRoles: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const body = await listRoles(projectId); + + return { + status: 200, + body, + }; + }, + + createProjectRole: async ({ + request: req, + params: { projectId }, + body, + }) => { + const perms = await authUser(req, { id: projectId }); + + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await createRole(projectId, body); + + return { + status: 201, + body: resBody, + }; + }, + + patchProjectRoles: async ({ + request: req, + params: { projectId }, + body, + }) => { + const perms = await authUser(req, { id: projectId }); + + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await patchRoles(projectId, body); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 200, + body: resBody, + }; + }, + + projectRoleMemberCounts: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const resBody = await countRolesMembers(projectId); + + return { + status: 200, + body: resBody, + }; + }, + + deleteProjectRole: async ({ + request: req, + params: { projectId, roleId }, + }) => { + const perms = await authUser(req, { id: projectId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await deleteRole(roleId); + + return { + status: 204, + body: resBody, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts index 0341b768f..63074e103 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts @@ -1,95 +1,124 @@ -import type { Project, ProjectPlugin } from '@prisma/client' +import { + editStrippers, + populatePluginManifests, + servicesInfos, +} from '@cpn-console/hooks'; +import type { ZoneObject } from '@cpn-console/hooks'; import type { - PermissionTarget, - PluginsUpdateBody, - ServiceUrl, -} from '@cpn-console/shared' -import { editStrippers, populatePluginManifests, servicesInfos } from '@cpn-console/hooks' -import type { ZoneObject } from '@cpn-console/hooks' + PermissionTarget, + PluginsUpdateBody, + ServiceUrl, +} from '@cpn-console/shared'; import { - getAdminPlugin, - getProjectInfosByIdOrThrow, - getProjectStore, - getPublicClusters, - saveProjectStore, -} from '@old-server/resources/queries-index.js' + getAdminPlugin, + getProjectInfosByIdOrThrow, + getProjectStore, + getPublicClusters, + saveProjectStore, +} from '@old-server/resources/queries-index.js'; +import type { Project, ProjectPlugin } from '@prisma/client'; export type ConfigRecords = { - key: string - pluginName: string - value: string | number | null -}[] + key: string; + pluginName: string; + value: string | number | null; +}[]; -export function dbToObj(records: Omit[]): PluginsUpdateBody { - const obj: PluginsUpdateBody = {} - for (const record of records) { - if (!obj[record.pluginName]) obj[record.pluginName] = {} - obj[record.pluginName][record.key] = record.value - } - return obj +export function dbToObj( + records: Omit[], +): PluginsUpdateBody { + const obj: PluginsUpdateBody = {}; + for (const record of records) { + if (!obj[record.pluginName]) obj[record.pluginName] = {}; + obj[record.pluginName][record.key] = record.value; + } + return obj; } export function objToDb(obj: PluginsUpdateBody): ConfigRecords { - return Object.entries(obj) - .map(([pluginName, values]) => Object.entries(values) - .map(([key, value]) => ({ pluginName, key, value }))) - .flat() + return Object.entries(obj) + .map(([pluginName, values]) => + Object.entries(values).map(([key, value]) => ({ + pluginName, + key, + value, + })), + ) + .flat(); } -export async function getProjectServices(projectId: Project['id'], permissionTarget: PermissionTarget) { - // Pré-requis - const project = await getProjectInfosByIdOrThrow(projectId) +export async function getProjectServices( + projectId: Project['id'], + permissionTarget: PermissionTarget, +) { + // Pré-requis + const project = await getProjectInfosByIdOrThrow(projectId); - const [projectStore, globalConfig] = await Promise.all([ - getProjectStore(projectId), - getAdminPlugin(), - ]) - const store = dbToObj([...projectStore, ...globalConfig]) + const [projectStore, globalConfig] = await Promise.all([ + getProjectStore(projectId), + getAdminPlugin(), + ]); + const store = dbToObj([...projectStore, ...globalConfig]); - const publicClusters = await getPublicClusters() - project.clusters = project.clusters.concat(publicClusters) - const zones: Map = new Map() // Pour dédoublonnage des zones - project.clusters.map(c => zones.set(c.zone.id, c.zone)) + const publicClusters = await getPublicClusters(); + project.clusters = project.clusters.concat(publicClusters); + const zones: Map = new Map(); // Pour dédoublonnage des zones + project.clusters.map((c) => zones.set(c.zone.id, c.zone)); - return Object.values(servicesInfos).map(({ name, title, to, imgSrc, description }) => { - let urls: ServiceUrl[] = [] - const toResponse = to - ? to({ - clusters: project.clusters, - zones: Array.from(zones.values()), - environments: project.environments, - project, - store, + return Object.values(servicesInfos) + .map(({ name, title, to, imgSrc, description }) => { + let urls: ServiceUrl[] = []; + const toResponse = to + ? to({ + clusters: project.clusters, + zones: Array.from(zones.values()), + environments: project.environments, + project, + store, + }) + : []; + if (Array.isArray(toResponse)) { + urls = toResponse.map((res) => ({ + name: res.title ?? '', + description: res.description ?? '', + to: res.to, + })); + } else if (typeof toResponse === 'string') { + urls = [{ to: toResponse, name: '' }]; + } else if (toResponse) { + urls = [{ name: toResponse.title ?? '', to: toResponse.to }]; + } + const manifest = populatePluginManifests({ + data: { + project: projectStore, + global: globalConfig, + }, + permissionTarget, + pluginName: name, + select: { + global: true, + project: true, + }, + }); + return { imgSrc, title, name, urls, manifest, description }; }) - : [] - if (Array.isArray(toResponse)) { - urls = toResponse.map(res => ({ name: res.title ?? '', description: res.description ?? '', to: res.to })) - } else if (typeof toResponse === 'string') { - urls = [{ to: toResponse, name: '' }] - } else if (toResponse) { - urls = [{ name: toResponse.title ?? '', to: toResponse.to }] - } - const manifest = populatePluginManifests({ - data: { - project: projectStore, - global: globalConfig, - }, - permissionTarget, - pluginName: name, - select: { - global: true, - project: true, - }, - }) - return { imgSrc, title, name, urls, manifest, description } - }).filter(s => s.urls.length || s.manifest.global?.length || s.manifest.project?.length) + .filter( + (s) => + s.urls.length || + s.manifest.global?.length || + s.manifest.project?.length, + ); } -export async function updateProjectServices(projectId: Project['id'], data: PluginsUpdateBody, stripperRoles: Array<'user' | 'admin'>) { - for (const role of stripperRoles) { - const parsedData = editStrippers.project[role].safeParse(data) - if (!parsedData.success) continue - await saveProjectStore(objToDb(parsedData.data), projectId) - } - return null +export async function updateProjectServices( + projectId: Project['id'], + data: PluginsUpdateBody, + stripperRoles: Array<'user' | 'admin'>, +) { + for (const role of stripperRoles) { + const parsedData = editStrippers.project[role].safeParse(data); + if (!parsedData.success) continue; + await saveProjectStore(objToDb(parsedData.data), projectId); + } + return null; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts index 19e7ddea7..470bcc1db 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts @@ -1,54 +1,58 @@ -import type { Project } from '@prisma/client' -import type { ConfigRecords } from './business.js' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { Project } from '@prisma/client'; + +import type { ConfigRecords } from './business.js'; // CONFIG export function getProjectStore(projectId: Project['id']) { - return prisma.projectPlugin.findMany({ - where: { projectId }, - select: { - key: true, - pluginName: true, - value: true, - }, - }) + return prisma.projectPlugin.findMany({ + where: { projectId }, + select: { + key: true, + pluginName: true, + value: true, + }, + }); } -export const getAdminPlugin = prisma.adminPlugin.findMany +export const getAdminPlugin = prisma.adminPlugin.findMany; -export async function saveProjectStore(records: ConfigRecords, projectId: Project['id']) { - for (const { pluginName, key, value } of records) { - if (value === null) { - await prisma.projectPlugin.delete({ - where: { - projectId_pluginName_key: { - projectId, - pluginName, - key, - }, - }, - }) - } else { - await prisma.projectPlugin.upsert({ - create: { - pluginName, - projectId, - key, - value: value.toString(), - }, - update: { - key, - value: value.toString(), - pluginName, - }, - where: { - projectId_pluginName_key: { - projectId, - pluginName, - key, - }, - }, - }) +export async function saveProjectStore( + records: ConfigRecords, + projectId: Project['id'], +) { + for (const { pluginName, key, value } of records) { + if (value === null) { + await prisma.projectPlugin.delete({ + where: { + projectId_pluginName_key: { + projectId, + pluginName, + key, + }, + }, + }); + } else { + await prisma.projectPlugin.upsert({ + create: { + pluginName, + projectId, + key, + value: value.toString(), + }, + update: { + key, + value: value.toString(), + pluginName, + }, + where: { + projectId_pluginName_key: { + projectId, + pluginName, + key, + }, + }, + }); + } } - } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts index 8df654e90..45e37dc77 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts @@ -1,160 +1,256 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { PROJECT_PERMS, projectServiceContract } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetServicesMock = vi.spyOn(business, 'getProjectServices') -const businessUpdateServicesMock = vi.spyOn(business, 'updateProjectServices') +import { PROJECT_PERMS, projectServiceContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessGetServicesMock = vi.spyOn(business, 'getProjectServices'); +const businessUpdateServicesMock = vi.spyOn(business, 'updateProjectServices'); describe('projectServiceRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const projectId = faker.string.uuid() - - describe('getServices', () => { - it('should return services for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetServicesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) - .query({ permissionTarget: 'user' }) - .end() - - expect(businessGetServicesMock).toHaveBeenCalledWith(projectId, 'user') - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should not return admin services for non admin', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetServicesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) - .query({ permissionTarget: 'admin' }) - .end() - - expect(businessGetServicesMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - - it('should return services for admin', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetServicesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) - .end() - - expect(businessGetServicesMock).toHaveBeenCalledWith(projectId, 'user') - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - }) - - describe('updateProjectServices', () => { - const updateData = { serviceA: { param1: 'value' } } - - it('should update services for project manager', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateServicesMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(businessUpdateServicesMock).toHaveBeenCalledWith(projectId, updateData, ['user']) - expect(response.statusCode).toEqual(204) - }) - - it('should update services for project admin', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateServicesMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(businessUpdateServicesMock).toHaveBeenCalledWith(projectId, updateData, ['user', 'admin']) - expect(response.statusCode).toEqual(204) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + const projectId = faker.string.uuid(); + + describe('getServices', () => { + it('should return services for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessGetServicesMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .get( + projectServiceContract.getServices.path.replace( + ':projectId', + projectId, + ), + ) + .query({ permissionTarget: 'user' }) + .end(); + + expect(businessGetServicesMock).toHaveBeenCalledWith( + projectId, + 'user', + ); + expect(response.statusCode).toEqual(200); + expect(response.json()).toEqual([]); + }); + + it('should not return admin services for non admin', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessGetServicesMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .get( + projectServiceContract.getServices.path.replace( + ':projectId', + projectId, + ), + ) + .query({ permissionTarget: 'admin' }) + .end(); + + expect(businessGetServicesMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + + it('should return services for admin', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.GUEST, + }); + const user = getUserMockInfos(true, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessGetServicesMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .get( + projectServiceContract.getServices.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(businessGetServicesMock).toHaveBeenCalledWith( + projectId, + 'user', + ); + expect(response.statusCode).toEqual(200); + expect(response.json()).toEqual([]); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + projectServiceContract.getServices.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(404); + }); + }); + + describe('updateProjectServices', () => { + const updateData = { serviceA: { param1: 'value' } }; + + it('should update services for project manager', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateServicesMock.mockResolvedValueOnce(null); + + const response = await app + .inject() + .post( + projectServiceContract.updateProjectServices.path.replace( + ':projectId', + projectId, + ), + ) + .body(updateData) + .end(); + + expect(businessUpdateServicesMock).toHaveBeenCalledWith( + projectId, + updateData, + ['user'], + ); + expect(response.statusCode).toEqual(204); + }); + + it('should update services for project admin', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE, + }); + const user = getUserMockInfos(true, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateServicesMock.mockResolvedValueOnce(null); + + const response = await app + .inject() + .post( + projectServiceContract.updateProjectServices.path.replace( + ':projectId', + projectId, + ), + ) + .body(updateData) + .end(); + + expect(businessUpdateServicesMock).toHaveBeenCalledWith( + projectId, + updateData, + ['user', 'admin'], + ); + expect(response.statusCode).toEqual(204); + }); + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectServiceContract.updateProjectServices.path.replace( + ':projectId', + projectId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectServiceContract.updateProjectServices.path.replace( + ':projectId', + projectId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + projectServiceContract.updateProjectServices.path.replace( + ':projectId', + projectId, + ), + ) + .body(updateData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts index c0713e5ea..5060c4914 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts @@ -1,38 +1,69 @@ -import { AdminAuthorized, ProjectAuthorized, projectServiceContract } from '@cpn-console/shared' -import { getProjectServices, updateProjectServices } from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { Forbidden403, NotFound404 } from '@old-server/utils/errors.js' +import { + AdminAuthorized, + ProjectAuthorized, + projectServiceContract, +} from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { Forbidden403, NotFound404 } from '@old-server/utils/errors.js'; + +import { getProjectServices, updateProjectServices } from './business.js'; export function projectServiceRouter() { - return serverInstance.router(projectServiceContract, { - // Récupérer les services d'un projet - getServices: async ({ request: req, params: { projectId }, query }) => { - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!AdminAuthorized.isAdmin(perms.adminPermissions) && query.permissionTarget === 'admin') return new Forbidden403('Vous ne pouvez pas demander les paramètres admin') + return serverInstance.router(projectServiceContract, { + // Récupérer les services d'un projet + getServices: async ({ request: req, params: { projectId }, query }) => { + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if ( + !AdminAuthorized.isAdmin(perms.adminPermissions) && + query.permissionTarget === 'admin' + ) + return new Forbidden403( + 'Vous ne pouvez pas demander les paramètres admin', + ); - const body = await getProjectServices(projectId, query.permissionTarget) + const body = await getProjectServices( + projectId, + query.permissionTarget, + ); - return { - status: 200, - body, - } - }, + return { + status: 200, + body, + }; + }, - updateProjectServices: async ({ request: req, params: { projectId }, body }) => { - const perms = await authUser(req, { id: projectId }) - if (!ProjectAuthorized.Manage(perms)) return new NotFound404() - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + updateProjectServices: async ({ + request: req, + params: { projectId }, + body, + }) => { + const perms = await authUser(req, { id: projectId }); + if (!ProjectAuthorized.Manage(perms)) return new NotFound404(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); - const allowedRoles: Array<'user' | 'admin'> = AdminAuthorized.isAdmin(perms.adminPermissions) ? ['user', 'admin'] : ['user'] + const allowedRoles: Array<'user' | 'admin'> = + AdminAuthorized.isAdmin(perms.adminPermissions) + ? ['user', 'admin'] + : ['user']; - const resBody = await updateProjectServices(projectId, body, allowedRoles) - return { - status: 204, - body: resBody, - } - }, - }) + const resBody = await updateProjectServices( + projectId, + body, + allowedRoles, + ); + return { + status: 204, + body: resBody, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts index 5d064cd5a..1ff117974 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts @@ -1,361 +1,518 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Cluster, Project, ProjectMembers, ProjectRole, User } from '@prisma/client' -import prisma from '../../__mocks__/prisma.js' -import { hook } from '../../__mocks__/utils/hook-wrapper.ts' -import { dbToObj } from '../project-service/business.ts' -import * as userBusiness from '../user/business.js' +import { faker } from '@faker-js/faker'; +import type { + Cluster, + Project, + ProjectMembers, + ProjectRole, + User, +} from '@prisma/client'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import { hook } from '../../__mocks__/utils/hook-wrapper.ts'; import { - BadRequest400, - ErrorResType, - Unprocessable422, -} from '../../utils/errors.js' -import { archiveProject, chunk, createProject, generateProjectsData, generateSlug, getProjectSecrets, listProjects, replayHooks, updateProject } from './business.ts' + BadRequest400, + ErrorResType, + Unprocessable422, +} from '../../utils/errors.js'; +import { dbToObj } from '../project-service/business.ts'; +import * as userBusiness from '../user/business.js'; +import { + archiveProject, + chunk, + createProject, + generateProjectsData, + generateSlug, + getProjectSecrets, + listProjects, + replayHooks, + updateProject, +} from './business.ts'; vi.mock('../../utils/hook-wrapper.ts', async () => ({ - hook, -})) + hook, +})); -const logViaSessionMock = vi.spyOn(userBusiness, 'logViaSession') +const logViaSessionMock = vi.spyOn(userBusiness, 'logViaSession'); -const projectId = faker.string.uuid() +const projectId = faker.string.uuid(); const user: User = { - id: faker.string.uuid(), - createdAt: new Date(), - updatedAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - adminRoleIds: [], - type: 'human', - lastLogin: null, -} + id: faker.string.uuid(), + createdAt: new Date(), + updatedAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + adminRoleIds: [], + type: 'human', + lastLogin: null, +}; const project: Project & { - clusters: Pick[] - members: ProjectMembers[] - roles: ProjectRole[] - owner: User + clusters: Pick[]; + members: ProjectMembers[]; + roles: ProjectRole[]; + owner: User; } = { - createdAt: new Date(), - updatedAt: new Date(), - description: '', - everyonePerms: 649n, - id: faker.string.uuid(), - locked: false, - name: faker.string.alphanumeric(8), - status: 'created', - ownerId: faker.string.uuid(), - clusters: [], - roles: [], - members: [], -} -const reqId = faker.string.uuid() + createdAt: new Date(), + updatedAt: new Date(), + description: '', + everyonePerms: 649n, + id: faker.string.uuid(), + locked: false, + name: faker.string.alphanumeric(8), + status: 'created', + ownerId: faker.string.uuid(), + clusters: [], + roles: [], + members: [], +}; +const reqId = faker.string.uuid(); describe('test project business utils', () => { - it('should transform arrow ', async () => { - const result = dbToObj([{ key: 'test', pluginName: 'test', value: 'test' }]) - expect(result).toEqual({ test: { test: 'test' } }) - }) -}) + it('should transform arrow ', async () => { + const result = dbToObj([ + { key: 'test', pluginName: 'test', value: 'test' }, + ]); + expect(result).toEqual({ test: { test: 'test' } }); + }); +}); describe('test project business logic', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - describe('listProjects', () => { - it('should return stringified perms', async () => { - prisma.project.findMany.mockResolvedValue([{ everyonePerms: 5n, clusters: [], roles: [{ permissions: 28n }] }]) - const response = await listProjects({}, user.id) - expect(response[0].everyonePerms).toBe('5') - expect(response[0].roles[0].permissions).toBe('28') - }) - }) - describe('getProjectSecrets', () => { - const getResultsHook = { - failed: false, - args: {}, - results: { - registry: { - secrets: { - token: 'myToken', - }, - status: { + beforeEach(() => { + vi.resetAllMocks(); + }); + describe('listProjects', () => { + it('should return stringified perms', async () => { + prisma.project.findMany.mockResolvedValue([ + { + everyonePerms: 5n, + clusters: [], + roles: [{ permissions: 28n }], + }, + ]); + const response = await listProjects({}, user.id); + expect(response[0].everyonePerms).toBe('5'); + expect(response[0].roles[0].permissions).toBe('28'); + }); + }); + describe('getProjectSecrets', () => { + const getResultsHook = { failed: false, - }, - }, - }, - } - it('should return transform secret', async () => { - hook.project.getSecrets.mockResolvedValue(getResultsHook) - - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) - const response = await getProjectSecrets(projectId) - - // according to src/utils/mocks.ts - expect(JSON.stringify(response)).toContain('myToken') - }) - - it('should return projects secrets', async () => { - hook.project.getSecrets.mockResolvedValue(getResultsHook) - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) - prisma.project.findMany.mockResolvedValue({ id: projectId }) - const response = await getProjectSecrets(projectId) - // according to src/utils/mocks.ts - expect(JSON.stringify(response)).toContain('myToken') - }) - - it('should return hook error', async () => { - hook.project.getSecrets.mockResolvedValue({ failed: true }) - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) - prisma.project.findMany.mockResolvedValue({ id: projectId }) - const response = await getProjectSecrets(projectId) - // according to src/utils/mocks.ts - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - - describe('createProject', () => { - it('should create project', async () => { - logViaSessionMock.mockResolvedValue({ user }) - - prisma.project.create.mockResolvedValue({ ...project, status: 'initializing' }) - prisma.project.findFirst.mockResolvedValue(undefined) - prisma.project.findMany.mockResolvedValue([]) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const projectRes = await createProject(project, user, reqId) - - expect(projectRes.name).toEqual(project.name) - expect(prisma.project.create).toHaveBeenCalledTimes(1) - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - }) - - it('should return plugins failed', async () => { - logViaSessionMock.mockResolvedValue({ user }) - - prisma.project.create.mockResolvedValue({ ...project, status: 'initializing' }) - prisma.project.findFirst.mockResolvedValue(undefined) - prisma.project.findMany.mockResolvedValue([]) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const response = await createProject(project, user, reqId) - - expect(prisma.project.create).toHaveBeenCalledTimes(1) - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - describe('updateProject', () => { - const updatedProjet = { - description: faker.lorem.lines(2), - everyonePerms: '5', - } - const reqId = faker.string.uuid() - const members: ProjectMembers[] = [{ userId: faker.string.uuid(), projectId: project.id, roleIds: [], user: { type: 'human' } }] - it('should update project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members }) - prisma.project.update.mockResolvedValue(project) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - await updateProject({ ...updatedProjet, ownerId: members[0].userId }, project.id, user, reqId) - - expect(prisma.project.update).toHaveBeenCalledTimes(2) - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - }) - - it('should update nothing', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members }) - prisma.project.update.mockResolvedValue(project) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - await updateProject({ }, project.id, user, reqId) - - expect(prisma.project.update).toHaveBeenCalledTimes(0) - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - }) - - it('should not update if project archived', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, status: 'archived' }) - prisma.project.update.mockResolvedValue(project) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const response = await updateProject({ }, project.id, user, reqId) - - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.project.update).toHaveBeenCalledTimes(0) - expect(prisma.log.create).toHaveBeenCalledTimes(0) - expect(hook.project.upsert).toHaveBeenCalledTimes(0) - }) - - it('should not update project, cause missing member', async () => { - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - logViaSessionMock.mockResolvedValue({ user }) - - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members: [] }) - - const response = await updateProject({ ownerId: members[0].userId }, project.id, user, reqId) - - expect(prisma.project.findUniqueOrThrow).toHaveBeenCalledTimes(1) - expect(response).toBeInstanceOf(BadRequest400) - expect(hook.project.upsert).toHaveBeenCalledTimes(0) - expect(prisma.log.update).toHaveBeenCalledTimes(0) - }) - - it('should return plugins failed', async () => { - logViaSessionMock.mockResolvedValue({ user }) - - prisma.project.findUniqueOrThrow.mockResolvedValue({ status: 'created' }) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const response = await updateProject(updatedProjet, project.id, user, reqId) - - expect(prisma.project.update).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - describe('replayHooks', () => { - const reqId = faker.string.uuid() - - it('should replay hooks', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'created' }) - hook.project.upsert.mockResolvedValue({ results: { failed: false } }) - - await replayHooks(project.id, user, reqId) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - }) - - it('should not replay hooks on archived project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'archived' }) - hook.project.upsert.mockResolvedValue({ results: { failed: false } }) - - const response = await replayHooks(project.id, user, reqId) - - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.log.create).toHaveBeenCalledTimes(0) - expect(hook.project.upsert).toHaveBeenCalledTimes(0) - }) - - it('should not replay hooks on locked project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: true, status: 'created' }) - hook.project.upsert.mockResolvedValue({ results: { failed: false } }) - - const response = await replayHooks(project.id, user, reqId) - - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.log.create).toHaveBeenCalledTimes(0) - expect(hook.project.upsert).toHaveBeenCalledTimes(0) - }) - - it('should update nothing and return error', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'created' }) - hook.project.upsert.mockResolvedValue({ results: { failed: true } }) - - const response = await replayHooks(project.id, user, reqId) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - - describe('archiveProject', () => { - it('should archive project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false }) - hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) - const response = await archiveProject(project.id, user, reqId) - expect(response).toBeNull() - expect(prisma.project.update).toHaveBeenLastCalledWith({ - where: { id: project.id }, - data: { - clusters: { set: [] }, - }, - }) - }) - - it('should not archive a project already archived', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false, status: 'archived' }) - hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) - const response = await archiveProject(project.id, user, reqId) - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.project.update).toHaveBeenCalledTimes(0) - }) - - it('should not archive a project locked', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: true, status: 'created' }) - hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) - const response = await archiveProject(project.id, user, reqId) - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.project.update).toHaveBeenCalledTimes(0) - }) - - it('should return hook fail', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false }) - hook.project.delete.mockResolvedValue({ results: { failed: true }, project: Promise.resolve({ status: 'failed' }) }) - const response = await archiveProject(project.id, user, reqId) - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - - describe('generateProjectsData', () => { - it('shoud return string, very bad test ...', async () => { - prisma.project.findMany.mockResolvedValue([{ name: 'test' }]) - const response = await generateProjectsData() - expect(response).toBeTypeOf('string') - }) - }) -}) + args: {}, + results: { + registry: { + secrets: { + token: 'myToken', + }, + status: { + failed: false, + }, + }, + }, + }; + it('should return transform secret', async () => { + hook.project.getSecrets.mockResolvedValue(getResultsHook); + + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + }); + const response = await getProjectSecrets(projectId); + + // according to src/utils/mocks.ts + expect(JSON.stringify(response)).toContain('myToken'); + }); + + it('should return projects secrets', async () => { + hook.project.getSecrets.mockResolvedValue(getResultsHook); + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + }); + prisma.project.findMany.mockResolvedValue({ id: projectId }); + const response = await getProjectSecrets(projectId); + // according to src/utils/mocks.ts + expect(JSON.stringify(response)).toContain('myToken'); + }); + + it('should return hook error', async () => { + hook.project.getSecrets.mockResolvedValue({ failed: true }); + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + }); + prisma.project.findMany.mockResolvedValue({ id: projectId }); + const response = await getProjectSecrets(projectId); + // according to src/utils/mocks.ts + expect(response).toBeInstanceOf(Unprocessable422); + }); + }); + + describe('createProject', () => { + it('should create project', async () => { + logViaSessionMock.mockResolvedValue({ user }); + + prisma.project.create.mockResolvedValue({ + ...project, + status: 'initializing', + }); + prisma.project.findFirst.mockResolvedValue(undefined); + prisma.project.findMany.mockResolvedValue([]); + hook.project.upsert.mockResolvedValue({ + results: {}, + project: { ...project }, + }); + + const projectRes = await createProject(project, user, reqId); + + expect(projectRes.name).toEqual(project.name); + expect(prisma.project.create).toHaveBeenCalledTimes(1); + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + }); + + it('should return plugins failed', async () => { + logViaSessionMock.mockResolvedValue({ user }); + + prisma.project.create.mockResolvedValue({ + ...project, + status: 'initializing', + }); + prisma.project.findFirst.mockResolvedValue(undefined); + prisma.project.findMany.mockResolvedValue([]); + hook.project.upsert.mockResolvedValue({ + results: { failed: true }, + project: { ...project }, + }); + + const response = await createProject(project, user, reqId); + + expect(prisma.project.create).toHaveBeenCalledTimes(1); + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(response).toBeInstanceOf(Unprocessable422); + }); + }); + describe('updateProject', () => { + const updatedProjet = { + description: faker.lorem.lines(2), + everyonePerms: '5', + }; + const reqId = faker.string.uuid(); + const members: ProjectMembers[] = [ + { + userId: faker.string.uuid(), + projectId: project.id, + roleIds: [], + user: { type: 'human' }, + }, + ]; + it('should update project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + members, + }); + prisma.project.update.mockResolvedValue(project); + hook.project.upsert.mockResolvedValue({ + results: {}, + project: { ...project }, + }); + + await updateProject( + { ...updatedProjet, ownerId: members[0].userId }, + project.id, + user, + reqId, + ); + + expect(prisma.project.update).toHaveBeenCalledTimes(2); + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + }); + + it('should update nothing', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + members, + }); + prisma.project.update.mockResolvedValue(project); + hook.project.upsert.mockResolvedValue({ + results: {}, + project: { ...project }, + }); + + await updateProject({}, project.id, user, reqId); + + expect(prisma.project.update).toHaveBeenCalledTimes(0); + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + }); + + it('should not update if project archived', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + status: 'archived', + }); + prisma.project.update.mockResolvedValue(project); + hook.project.upsert.mockResolvedValue({ + results: {}, + project: { ...project }, + }); + + const response = await updateProject({}, project.id, user, reqId); + + expect(response).toBeInstanceOf(ErrorResType); + expect(prisma.project.update).toHaveBeenCalledTimes(0); + expect(prisma.log.create).toHaveBeenCalledTimes(0); + expect(hook.project.upsert).toHaveBeenCalledTimes(0); + }); + + it('should not update project, cause missing member', async () => { + hook.project.upsert.mockResolvedValue({ + results: {}, + project: { ...project }, + }); + logViaSessionMock.mockResolvedValue({ user }); + + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + members: [], + }); + + const response = await updateProject( + { ownerId: members[0].userId }, + project.id, + user, + reqId, + ); + + expect(prisma.project.findUniqueOrThrow).toHaveBeenCalledTimes(1); + expect(response).toBeInstanceOf(BadRequest400); + expect(hook.project.upsert).toHaveBeenCalledTimes(0); + expect(prisma.log.update).toHaveBeenCalledTimes(0); + }); + + it('should return plugins failed', async () => { + logViaSessionMock.mockResolvedValue({ user }); + + prisma.project.findUniqueOrThrow.mockResolvedValue({ + status: 'created', + }); + hook.project.upsert.mockResolvedValue({ + results: { failed: true }, + project: { ...project }, + }); + + const response = await updateProject( + updatedProjet, + project.id, + user, + reqId, + ); + + expect(prisma.project.update).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(response).toBeInstanceOf(Unprocessable422); + }); + }); + describe('replayHooks', () => { + const reqId = faker.string.uuid(); + + it('should replay hooks', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + locked: false, + status: 'created', + }); + hook.project.upsert.mockResolvedValue({ + results: { failed: false }, + }); + + await replayHooks(project.id, user, reqId); + + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + }); + + it('should not replay hooks on archived project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + locked: false, + status: 'archived', + }); + hook.project.upsert.mockResolvedValue({ + results: { failed: false }, + }); + + const response = await replayHooks(project.id, user, reqId); + + expect(response).toBeInstanceOf(ErrorResType); + expect(prisma.log.create).toHaveBeenCalledTimes(0); + expect(hook.project.upsert).toHaveBeenCalledTimes(0); + }); + + it('should not replay hooks on locked project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + locked: true, + status: 'created', + }); + hook.project.upsert.mockResolvedValue({ + results: { failed: false }, + }); + + const response = await replayHooks(project.id, user, reqId); + + expect(response).toBeInstanceOf(ErrorResType); + expect(prisma.log.create).toHaveBeenCalledTimes(0); + expect(hook.project.upsert).toHaveBeenCalledTimes(0); + }); + + it('should update nothing and return error', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + locked: false, + status: 'created', + }); + hook.project.upsert.mockResolvedValue({ + results: { failed: true }, + }); + + const response = await replayHooks(project.id, user, reqId); + + expect(prisma.log.create).toHaveBeenCalledTimes(1); + expect(hook.project.upsert).toHaveBeenCalledTimes(1); + expect(response).toBeInstanceOf(Unprocessable422); + }); + }); + + describe('archiveProject', () => { + it('should archive project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + locked: false, + }); + hook.project.delete.mockResolvedValue({ + results: { failed: false }, + project: Promise.resolve({ status: 'archived' }), + }); + const response = await archiveProject(project.id, user, reqId); + expect(response).toBeNull(); + expect(prisma.project.update).toHaveBeenLastCalledWith({ + where: { id: project.id }, + data: { + clusters: { set: [] }, + }, + }); + }); + + it('should not archive a project already archived', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + locked: false, + status: 'archived', + }); + hook.project.delete.mockResolvedValue({ + results: { failed: false }, + project: Promise.resolve({ status: 'archived' }), + }); + const response = await archiveProject(project.id, user, reqId); + expect(response).toBeInstanceOf(ErrorResType); + expect(prisma.project.update).toHaveBeenCalledTimes(0); + }); + + it('should not archive a project locked', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + locked: true, + status: 'created', + }); + hook.project.delete.mockResolvedValue({ + results: { failed: false }, + project: Promise.resolve({ status: 'archived' }), + }); + const response = await archiveProject(project.id, user, reqId); + expect(response).toBeInstanceOf(ErrorResType); + expect(prisma.project.update).toHaveBeenCalledTimes(0); + }); + + it('should return hook fail', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ + id: projectId, + locked: false, + }); + hook.project.delete.mockResolvedValue({ + results: { failed: true }, + project: Promise.resolve({ status: 'failed' }), + }); + const response = await archiveProject(project.id, user, reqId); + expect(response).toBeInstanceOf(Unprocessable422); + }); + }); + + describe('generateProjectsData', () => { + it('shoud return string, very bad test ...', async () => { + prisma.project.findMany.mockResolvedValue([{ name: 'test' }]); + const response = await generateProjectsData(); + expect(response).toBeTypeOf('string'); + }); + }); +}); describe('chunk function', () => { - it('should return 5 elements', () => { - const letters = ['A', 'B', 'C', 'D', 'E'] - expect(chunk(letters, 5)).toEqual([letters]) - }) - it('should return 3,2 elements', () => { - const letters = ['A', 'B', 'C', 'D', 'E'] - expect(chunk(letters, 3)).toEqual([['A', 'B', 'C'], ['D', 'E']]) - }) - it('should return 4 elements', () => { - const letters = ['A', 'B', 'C', 'D'] - expect(chunk(letters, 5)).toEqual([letters]) - }) -}) + it('should return 5 elements', () => { + const letters = ['A', 'B', 'C', 'D', 'E']; + expect(chunk(letters, 5)).toEqual([letters]); + }); + it('should return 3,2 elements', () => { + const letters = ['A', 'B', 'C', 'D', 'E']; + expect(chunk(letters, 3)).toEqual([ + ['A', 'B', 'C'], + ['D', 'E'], + ]); + }); + it('should return 4 elements', () => { + const letters = ['A', 'B', 'C', 'D']; + expect(chunk(letters, 5)).toEqual([letters]); + }); +}); describe('generateSlug', () => { - it('should return prefix, no array', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix) - expect(generated).toEqual(prefix) - }) - it('should return prefix, empty array', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, []) - expect(generated).toEqual(prefix) - }) - it('should return prefix, no match', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, [faker.string.alphanumeric(5), faker.string.alphanumeric(5)]) - expect(generated).toEqual(prefix) - }) - it('should return generated slug at 1 or 0, all matchs', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, [prefix]) - expect(generated).match(/-[01]$/) - }) - it('should return generated slug at 4, all matchs', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, [prefix, `${prefix}-0`, `${prefix}-1`, `${prefix}-2`, `${prefix}-3`]) - expect(generated).match(/-4$/) - }) - it('should fill empty space', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, [prefix, `${prefix}-0`, `${prefix}-1`, `${prefix}-3`]) - expect(generated).match(/-2$/) - }) -}) + it('should return prefix, no array', () => { + const prefix = faker.string.alphanumeric(5); + const generated = generateSlug(prefix); + expect(generated).toEqual(prefix); + }); + it('should return prefix, empty array', () => { + const prefix = faker.string.alphanumeric(5); + const generated = generateSlug(prefix, []); + expect(generated).toEqual(prefix); + }); + it('should return prefix, no match', () => { + const prefix = faker.string.alphanumeric(5); + const generated = generateSlug(prefix, [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ]); + expect(generated).toEqual(prefix); + }); + it('should return generated slug at 1 or 0, all matchs', () => { + const prefix = faker.string.alphanumeric(5); + const generated = generateSlug(prefix, [prefix]); + expect(generated).match(/-[01]$/); + }); + it('should return generated slug at 4, all matchs', () => { + const prefix = faker.string.alphanumeric(5); + const generated = generateSlug(prefix, [ + prefix, + `${prefix}-0`, + `${prefix}-1`, + `${prefix}-2`, + `${prefix}-3`, + ]); + expect(generated).match(/-4$/); + }); + it('should fill empty space', () => { + const prefix = faker.string.alphanumeric(5); + const generated = generateSlug(prefix, [ + prefix, + `${prefix}-0`, + `${prefix}-1`, + `${prefix}-3`, + ]); + expect(generated).match(/-2$/); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts index 713b99003..46974dcd0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts @@ -1,261 +1,405 @@ -import { json2csv } from 'json-2-csv' -import { servicesInfos } from '@cpn-console/hooks' -import type { Project, User } from '@prisma/client' -import type { projectContract } from '@cpn-console/shared' -import { ProjectStatusSchema } from '@cpn-console/shared' +import { servicesInfos } from '@cpn-console/hooks'; +import type { projectContract } from '@cpn-console/shared'; +import { ProjectStatusSchema } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; import { - addLogs, - deleteAllEnvironmentForProject, - deleteAllRepositoryForProject, - getAllProjectsDataForExport, - getProjectOrThrow, - getSlugs, - initializeProject, - listProjects as listProjectsQuery, - lockProject, - updateProject as updateProjectQuery, -} from '@old-server/resources/queries-index.js' -import type { ErrorResType } from '@old-server/utils/errors.js' -import { BadRequest400, Forbidden403, Unprocessable422 } from '@old-server/utils/errors.js' -import { whereBuilder } from '@old-server/utils/controller.js' -import { hook } from '@old-server/utils/hook-wrapper.js' -import type { UserDetails } from '@old-server/types/index.js' -import prisma from '@old-server/prisma.js' -import { parallelBulkLimit } from '@old-server/utils/env.js' + addLogs, + deleteAllEnvironmentForProject, + deleteAllRepositoryForProject, + getAllProjectsDataForExport, + getProjectOrThrow, + getSlugs, + initializeProject, + listProjects as listProjectsQuery, + lockProject, + updateProject as updateProjectQuery, +} from '@old-server/resources/queries-index.js'; +import type { UserDetails } from '@old-server/types/index.js'; +import { whereBuilder } from '@old-server/utils/controller.js'; +import { parallelBulkLimit } from '@old-server/utils/env.js'; +import type { ErrorResType } from '@old-server/utils/errors.js'; +import { + BadRequest400, + Forbidden403, + Unprocessable422, +} from '@old-server/utils/errors.js'; +import { hook } from '@old-server/utils/hook-wrapper.js'; +import type { Project, User } from '@prisma/client'; +import { json2csv } from 'json-2-csv'; export function generateSlug(prefix: string, existingSlugs?: string[]) { - if (!existingSlugs?.includes(prefix)) { - return prefix - } - let idx = 1 - let generated = `${prefix}-${idx}` - while (existingSlugs.includes(generated)) { - idx++ - generated = `${prefix}-${idx}` - } - return generated + if (!existingSlugs?.includes(prefix)) { + return prefix; + } + let idx = 1; + let generated = `${prefix}-${idx}`; + while (existingSlugs.includes(generated)) { + idx++; + generated = `${prefix}-${idx}`; + } + return generated; } -const projectStatus = ProjectStatusSchema._def.values -export async function listProjects({ status, statusIn, statusNotIn, filter = 'member', ...query }: typeof projectContract.listProjects.query._type, userId: User['id'] | undefined) { - return listProjectsQuery({ - ...query, - status: whereBuilder({ enumValues: projectStatus, eqValue: status, inValues: statusIn, notInValues: statusNotIn }), - filter, - userId, - }).then(projects => projects.map(({ clusters, ...project }) => ({ - ...project, - clusterIds: clusters.map(({ id }) => id), - roles: project.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), - everyonePerms: project.everyonePerms.toString(), - }))) +const projectStatus = ProjectStatusSchema._def.values; +export async function listProjects( + { + status, + statusIn, + statusNotIn, + filter = 'member', + ...query + }: typeof projectContract.listProjects.query._type, + userId: User['id'] | undefined, +) { + return listProjectsQuery({ + ...query, + status: whereBuilder({ + enumValues: projectStatus, + eqValue: status, + inValues: statusIn, + notInValues: statusNotIn, + }), + filter, + userId, + }).then((projects) => + projects.map(({ clusters, ...project }) => ({ + ...project, + clusterIds: clusters.map(({ id }) => id), + roles: project.roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + everyonePerms: project.everyonePerms.toString(), + })), + ); } export async function getProjectSecrets(projectId: string) { - const hookReply = await hook.project.getSecrets(projectId) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la récupération des secrets du projet') - } + const hookReply = await hook.project.getSecrets(projectId); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services à la récupération des secrets du projet', + ); + } - return Object.fromEntries( - Object.entries(hookReply.results) - // @ts-ignore - .filter(([_key, value]) => Object.keys(value.secrets).length) - // @ts-ignore - .map(([key, value]) => [servicesInfos[key]?.title, value.secrets]), - ) + return Object.fromEntries( + Object.entries(hookReply.results) + // @ts-ignore + .filter(([_key, value]) => Object.keys(value.secrets).length) + // @ts-ignore + .map(([key, value]) => [servicesInfos[key]?.title, value.secrets]), + ); } -export async function createProject(dataDto: typeof projectContract.createProject.body._type, requestor: UserDetails, requestId: string) { - if (requestor.type !== 'human') return new BadRequest400('Seuls les comptes humains peuvent créer des projets') +export async function createProject( + dataDto: typeof projectContract.createProject.body._type, + requestor: UserDetails, + requestId: string, +) { + if (requestor.type !== 'human') + return new BadRequest400( + 'Seuls les comptes humains peuvent créer des projets', + ); - let slug = dataDto.name - const projectsWithSamePrefix = await getSlugs(slug) - slug = generateSlug(slug, projectsWithSamePrefix?.map(project => project.slug)) + let slug = dataDto.name; + const projectsWithSamePrefix = await getSlugs(slug); + slug = generateSlug( + slug, + projectsWithSamePrefix?.map((project) => project.slug), + ); - // Actions - const project = await initializeProject({ ...dataDto, slug, ownerId: requestor.id }) + // Actions + const project = await initializeProject({ + ...dataDto, + slug, + ownerId: requestor.id, + }); - const { results, project: projectInfos } = await hook.project.upsert(project.id) - await addLogs({ action: 'Create Project', data: results, userId: requestor.id, requestId, projectId: project.id }) - if (results.failed) { - return new Unprocessable422('Echec des services à la création du projet') - } + const { results, project: projectInfos } = await hook.project.upsert( + project.id, + ); + await addLogs({ + action: 'Create Project', + data: results, + userId: requestor.id, + requestId, + projectId: project.id, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services à la création du projet', + ); + } - return { - ...projectInfos, - clusterIds: projectInfos.clusters.map(({ id }) => id), - everyonePerms: projectInfos.everyonePerms.toString(), - roles: projectInfos.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), - } + return { + ...projectInfos, + clusterIds: projectInfos.clusters.map(({ id }) => id), + everyonePerms: projectInfos.everyonePerms.toString(), + roles: projectInfos.roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + }; } export async function getProject(projectId: Project['id']) { - return getProjectOrThrow(projectId).then(({ clusters, ...project }) => ({ - ...project, - clusterIds: clusters.map(({ id }) => id), - roles: project.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), - everyonePerms: project.everyonePerms.toString(), - })) + return getProjectOrThrow(projectId).then(({ clusters, ...project }) => ({ + ...project, + clusterIds: clusters.map(({ id }) => id), + roles: project.roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + everyonePerms: project.everyonePerms.toString(), + })); } export async function updateProject( - { description, ownerId: ownerIdCandidate, everyonePerms, locked, ...data }: typeof projectContract.updateProject.body._type, - projectId: Project['id'], - requestor: UserDetails, - requestId: string, + { + description, + ownerId: ownerIdCandidate, + everyonePerms, + locked, + ...data + }: typeof projectContract.updateProject.body._type, + projectId: Project['id'], + requestor: UserDetails, + requestId: string, ) { - // Actions - const projectDb = await prisma.project.findUniqueOrThrow({ - where: { id: projectId }, - include: { members: { include: { user: true } } }, - }) + // Actions + const projectDb = await prisma.project.findUniqueOrThrow({ + where: { id: projectId }, + include: { members: { include: { user: true } } }, + }); - if (projectDb.status === 'archived') return new Forbidden403('Le projet est archivé') + if (projectDb.status === 'archived') + return new Forbidden403('Le projet est archivé'); - if (ownerIdCandidate && ownerIdCandidate !== projectDb.ownerId) { - const memberCandidate = projectDb.members.find(member => member.userId === ownerIdCandidate) - if (!memberCandidate) { - return new BadRequest400('Le nouveau propriétaire doit faire partie des membres actuels du projet') + if (ownerIdCandidate && ownerIdCandidate !== projectDb.ownerId) { + const memberCandidate = projectDb.members.find( + (member) => member.userId === ownerIdCandidate, + ); + if (!memberCandidate) { + return new BadRequest400( + 'Le nouveau propriétaire doit faire partie des membres actuels du projet', + ); + } + if (memberCandidate.user.type !== 'human') + return new BadRequest400( + 'Seuls les comptes humains peuvent être propriétaire de projets', + ); + if ( + !projectDb.members.find( + (member) => member.userId === projectDb.ownerId, + ) + ) { + await prisma.projectMembers.create({ + data: { userId: projectDb.ownerId, projectId }, + }); + } + await prisma.$transaction([ + prisma.projectMembers.delete({ + where: { + projectId_userId: { userId: ownerIdCandidate, projectId }, + }, + }), + prisma.project.update({ + where: { id: projectId }, + data: { ownerId: ownerIdCandidate }, + }), + ]); } - if (memberCandidate.user.type !== 'human') return new BadRequest400('Seuls les comptes humains peuvent être propriétaire de projets') - if (!projectDb.members.find(member => member.userId === projectDb.ownerId)) { - await prisma.projectMembers.create({ - data: { userId: projectDb.ownerId, projectId }, - }) - } - await prisma.$transaction([ - prisma.projectMembers.delete({ - where: { projectId_userId: { userId: ownerIdCandidate, projectId } }, - }), - prisma.project.update({ where: { id: projectId }, data: { ownerId: ownerIdCandidate } }), - ]) - } - if (typeof description !== 'undefined' || typeof everyonePerms !== 'undefined' || typeof locked !== 'undefined') { - await updateProjectQuery(projectId, { - description, - locked, - ...everyonePerms && { everyonePerms: BigInt(everyonePerms) }, - ...data, - }) - } + if ( + typeof description !== 'undefined' || + typeof everyonePerms !== 'undefined' || + typeof locked !== 'undefined' + ) { + await updateProjectQuery(projectId, { + description, + locked, + ...(everyonePerms && { everyonePerms: BigInt(everyonePerms) }), + ...data, + }); + } - const { results, project: projectInfos } = await hook.project.upsert(projectId) - await addLogs({ action: 'Update Project', data: results, userId: requestor.id, requestId, projectId: projectInfos.id }) - if (results.failed) { - return new Unprocessable422('Echec des services à la mise à jour du projet') - } + const { results, project: projectInfos } = + await hook.project.upsert(projectId); + await addLogs({ + action: 'Update Project', + data: results, + userId: requestor.id, + requestId, + projectId: projectInfos.id, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services à la mise à jour du projet', + ); + } - return { - ...projectInfos, - clusterIds: projectInfos.clusters.map(({ id }) => id), - everyonePerms: projectInfos.everyonePerms.toString(), - roles: projectInfos.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), - } + return { + ...projectInfos, + clusterIds: projectInfos.clusters.map(({ id }) => id), + everyonePerms: projectInfos.everyonePerms.toString(), + roles: projectInfos.roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + }; } interface ReplayHooksArgs { - projectId: Project['id'] - userId?: User['id'] - requestId: string + projectId: Project['id']; + userId?: User['id']; + requestId: string; } -export async function replayHooks({ projectId, userId, requestId }: ReplayHooksArgs): Promise { - const projectDb = await prisma.project.findUniqueOrThrow({ - where: { id: projectId }, - include: { members: { include: { user: true } } }, - }) - if (projectDb.locked) return new Forbidden403('Le projet est verrouillé') - if (projectDb.status === 'archived') return new Forbidden403('Le projet est archivé') - // Actions - const { results } = await hook.project.upsert(projectId) - await addLogs({ action: 'Replay hooks for Project', data: results, userId, requestId, projectId }) - if (results.failed) { - return new Unprocessable422('Echec des services au reprovisionnement du projet') - } - return null +export async function replayHooks({ + projectId, + userId, + requestId, +}: ReplayHooksArgs): Promise { + const projectDb = await prisma.project.findUniqueOrThrow({ + where: { id: projectId }, + include: { members: { include: { user: true } } }, + }); + if (projectDb.locked) return new Forbidden403('Le projet est verrouillé'); + if (projectDb.status === 'archived') + return new Forbidden403('Le projet est archivé'); + // Actions + const { results } = await hook.project.upsert(projectId); + await addLogs({ + action: 'Replay hooks for Project', + data: results, + userId, + requestId, + projectId, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services au reprovisionnement du projet', + ); + } + return null; } -export async function archiveProject(projectId: Project['id'], requestor: UserDetails, requestId: string): Promise { - // Actions - // Empty the project first - const [projectDb, ..._] = await Promise.all([ - // get initial project state - prisma.project.findUniqueOrThrow({ where: { id: projectId } }), - deleteAllRepositoryForProject(projectId), - deleteAllEnvironmentForProject(projectId), - ]) +export async function archiveProject( + projectId: Project['id'], + requestor: UserDetails, + requestId: string, +): Promise { + // Actions + // Empty the project first + const [projectDb, ..._] = await Promise.all([ + // get initial project state + prisma.project.findUniqueOrThrow({ where: { id: projectId } }), + deleteAllRepositoryForProject(projectId), + deleteAllEnvironmentForProject(projectId), + ]); - if (projectDb.locked) return new Forbidden403('Le projet est verrouillé') - if (projectDb.status === 'archived') return new BadRequest400('Le projet est archivé') - if (projectDb.locked) { - await lockProject(projectId) - } + if (projectDb.locked) return new Forbidden403('Le projet est verrouillé'); + if (projectDb.status === 'archived') + return new BadRequest400('Le projet est archivé'); + if (projectDb.locked) { + await lockProject(projectId); + } - // -- début - Suppression projet -- - const { results, project } = await hook.project.delete(projectId) - await addLogs({ action: 'Delete all project resources', data: results, userId: requestor.id, requestId, projectId }) - if (project.status !== 'archived' && !projectDb.locked) { - await prisma.project.update({ where: { id: projectId }, data: { locked: false } }) - } - if (results.failed) { - return new Unprocessable422('Echec des services à la suppression du projet') - } + // -- début - Suppression projet -- + const { results, project } = await hook.project.delete(projectId); + await addLogs({ + action: 'Delete all project resources', + data: results, + userId: requestor.id, + requestId, + projectId, + }); + if (project.status !== 'archived' && !projectDb.locked) { + await prisma.project.update({ + where: { id: projectId }, + data: { locked: false }, + }); + } + if (results.failed) { + return new Unprocessable422( + 'Echec des services à la suppression du projet', + ); + } - // Retrait clusters -- - await prisma.project.update({ - where: { id: projectId }, - data: { - clusters: { set: [] }, - }, - }) + // Retrait clusters -- + await prisma.project.update({ + where: { id: projectId }, + data: { + clusters: { set: [] }, + }, + }); - // -- fin - Suppression projet -- - return null + // -- fin - Suppression projet -- + return null; } export async function generateProjectsData() { - const projects = await getAllProjectsDataForExport() + const projects = await getAllProjectsDataForExport(); - return json2csv(projects, { - emptyFieldValue: '', - }) + return json2csv(projects, { + emptyFieldValue: '', + }); } -export async function bulkActionProject(data: typeof projectContract.bulkActionProject.body._type, requestor: UserDetails, requestId: string) { - if (data.projectIds === 'all') { - data.projectIds = (await prisma.project.findMany({ - select: { id: true }, - where: { status: { not: 'archived' } }, - })).map(({ id }) => id) - } - bulkExector(data.projectIds - .map((projectId) => { - if (data.action === 'archive') { - return () => archiveProject(projectId, requestor, requestId) - } - if (data.action === 'lock') { - return () => updateProject({ locked: true }, projectId, requestor, requestId) - } - if (data.action === 'unlock') { - return () => updateProject({ locked: false }, projectId, requestor, requestId) - } - if (data.action === 'replay') { - return () => replayHooks({ projectId, userId: requestor.id, requestId }) - } - // should never been called - return async () => {} - })) +export async function bulkActionProject( + data: typeof projectContract.bulkActionProject.body._type, + requestor: UserDetails, + requestId: string, +) { + if (data.projectIds === 'all') { + data.projectIds = ( + await prisma.project.findMany({ + select: { id: true }, + where: { status: { not: 'archived' } }, + }) + ).map(({ id }) => id); + } + bulkExector( + data.projectIds.map((projectId) => { + if (data.action === 'archive') { + return () => archiveProject(projectId, requestor, requestId); + } + if (data.action === 'lock') { + return () => + updateProject( + { locked: true }, + projectId, + requestor, + requestId, + ); + } + if (data.action === 'unlock') { + return () => + updateProject( + { locked: false }, + projectId, + requestor, + requestId, + ); + } + if (data.action === 'replay') { + return () => + replayHooks({ projectId, userId: requestor.id, requestId }); + } + // should never been called + return async () => {}; + }), + ); } export function chunk(arr: T[], size: number): T[][] { - return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => - arr.slice(i * size, i * size + size)) + return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => + arr.slice(i * size, i * size + size), + ); } async function bulkExector(toExecute: Array<() => Promise>) { - const toExecuteChunked = chunk(toExecute, parallelBulkLimit) - for (const chunkToExecute of toExecuteChunked) { - await Promise.allSettled(chunkToExecute.map(fn => fn())) - } + const toExecuteChunked = chunk(toExecute, parallelBulkLimit); + for (const chunkToExecute of toExecuteChunked) { + await Promise.allSettled(chunkToExecute.map((fn) => fn())); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts index ea4ae31f8..419968b5a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts @@ -1,335 +1,366 @@ -import type { - Prisma, - Project, - User, -} from '@prisma/client' -import { - ProjectStatus, -} from '@prisma/client' -import type { XOR, projectContract } from '@cpn-console/shared' -import prisma from '@old-server/prisma.js' -import { appVersion } from '@old-server/utils/env.js' -import { uuid } from '@old-server/utils/queries-tools.js' +import type { XOR, projectContract } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; +import { appVersion } from '@old-server/utils/env.js'; +import { uuid } from '@old-server/utils/queries-tools.js'; +import type { Prisma, Project, User } from '@prisma/client'; +import { ProjectStatus } from '@prisma/client'; -type ProjectUpdate = Partial> +type ProjectUpdate = Partial< + Pick +>; export function updateProject(id: Project['id'], data: ProjectUpdate) { - return prisma.project.update({ - where: { id }, - data, - include: { members: true }, - }) + return prisma.project.update({ + where: { id }, + data, + include: { members: true }, + }); } // SELECT -type FilterWhere = XOR<{ - userId?: User['id'] - filter: 'all' -}, { - userId: User['id'] | undefined - filter: 'owned' | 'member' - }> -type ListProjectWhere = Omit<(typeof projectContract.listProjects.query._type), 'status_in' | 'status_not_in' | 'status'> & - Pick & - FilterWhere +type FilterWhere = XOR< + { + userId?: User['id']; + filter: 'all'; + }, + { + userId: User['id'] | undefined; + filter: 'owned' | 'member'; + } +>; +type ListProjectWhere = Omit< + typeof projectContract.listProjects.query._type, + 'status_in' | 'status_not_in' | 'status' +> & + Pick & + FilterWhere; export async function listProjects({ - description, - locked, - name, - status, - id, - filter, - userId, - search, - lastSuccessProvisionningVersion, + description, + locked, + name, + status, + id, + filter, + userId, + search, + lastSuccessProvisionningVersion, }: ListProjectWhere) { - const whereAnd: Prisma.ProjectWhereInput[] = [] - if (id) whereAnd.push({ id }) - if (locked != null) whereAnd.push({ locked }) - if (name) whereAnd.push({ name }) - if (status) whereAnd.push({ status }) - if (description) whereAnd.push({ description: { contains: description } }) - if (lastSuccessProvisionningVersion) { - if (lastSuccessProvisionningVersion === 'outdated') whereAnd.push({ lastSuccessProvisionningVersion: { not: appVersion } }) - else if (lastSuccessProvisionningVersion === 'last') whereAnd.push({ lastSuccessProvisionningVersion: { equals: appVersion } }) - else whereAnd.push({ lastSuccessProvisionningVersion }) - } - if (search) { - whereAnd.push({ OR: [{ - name: { contains: search }, - }, { - owner: { email: { contains: search } }, - }] }) - } + const whereAnd: Prisma.ProjectWhereInput[] = []; + if (id) whereAnd.push({ id }); + if (locked != null) whereAnd.push({ locked }); + if (name) whereAnd.push({ name }); + if (status) whereAnd.push({ status }); + if (description) whereAnd.push({ description: { contains: description } }); + if (lastSuccessProvisionningVersion) { + if (lastSuccessProvisionningVersion === 'outdated') + whereAnd.push({ + lastSuccessProvisionningVersion: { not: appVersion }, + }); + else if (lastSuccessProvisionningVersion === 'last') + whereAnd.push({ + lastSuccessProvisionningVersion: { equals: appVersion }, + }); + else whereAnd.push({ lastSuccessProvisionningVersion }); + } + if (search) { + whereAnd.push({ + OR: [ + { + name: { contains: search }, + }, + { + owner: { email: { contains: search } }, + }, + ], + }); + } - if (filter === 'owned') { - whereAnd.push({ ownerId: userId }) - } else if (filter === 'member') { - whereAnd.push({ OR: [{ - members: { some: { userId } }, - }, { - ownerId: userId, - }] }) - } + if (filter === 'owned') { + whereAnd.push({ ownerId: userId }); + } else if (filter === 'member') { + whereAnd.push({ + OR: [ + { + members: { some: { userId } }, + }, + { + ownerId: userId, + }, + ], + }); + } - return prisma.project.findMany({ - where: { AND: whereAnd }, - include: { - clusters: { select: { id: true } }, - members: { include: { user: true } }, - roles: true, - owner: true, - }, - }) + return prisma.project.findMany({ + where: { AND: whereAnd }, + include: { + clusters: { select: { id: true } }, + members: { include: { user: true } }, + roles: true, + owner: true, + }, + }); } export function getProjectOrThrow(id: Project['id'] | Project['slug']) { - return prisma.project.findFirstOrThrow({ - where: uuid.test(id) - ? { id } - : { slug: id }, - include: { - clusters: { select: { id: true } }, - members: { include: { user: true } }, - roles: true, - owner: true, - }, - }) + return prisma.project.findFirstOrThrow({ + where: uuid.test(id) ? { id } : { slug: id }, + include: { + clusters: { select: { id: true } }, + members: { include: { user: true } }, + roles: true, + owner: true, + }, + }); } export function getProjectInfosByIdOrThrow(projectId: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { - id: projectId, - }, - include: { - environments: true, - clusters: { include: { zone: true } }, - }, - }) + return prisma.project.findUniqueOrThrow({ + where: { + id: projectId, + }, + include: { + environments: true, + clusters: { include: { zone: true } }, + }, + }); } export function getProjectMembers(projectId: Project['id']) { - return prisma.projectMembers.findMany({ - where: { - projectId, - }, - include: { user: true }, - }) + return prisma.projectMembers.findMany({ + where: { + projectId, + }, + include: { user: true }, + }); } export function getProjectById(id: Project['id']) { - return prisma.project.findUnique({ where: { id } }) + return prisma.project.findUnique({ where: { id } }); } export const baseProjectIncludes = { - members: { include: { user: true } }, - clusters: true, - roles: true, - owner: true, -} as const + members: { include: { user: true } }, + clusters: true, + roles: true, + owner: true, +} as const; export function getProjectInfos(id: Project['id']) { - return prisma.project.findUnique({ - where: { id }, - include: baseProjectIncludes, - }) + return prisma.project.findUnique({ + where: { id }, + include: baseProjectIncludes, + }); } export function getProjectInfosOrThrow(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: baseProjectIncludes, - }) + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: baseProjectIncludes, + }); } export function getProjectInfosAndRepos(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: { - ...baseProjectIncludes, - repositories: true, - }, - }) + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: { + ...baseProjectIncludes, + repositories: true, + }, + }); } export function getSlugs(slugPrefix: string) { - return prisma.project.findMany({ - where: { - slug: { startsWith: slugPrefix }, - }, - }) + return prisma.project.findMany({ + where: { + slug: { startsWith: slugPrefix }, + }, + }); } export function getAllProjectsDataForExport() { - return prisma.project.findMany({ - select: { - name: true, - description: true, - createdAt: true, - updatedAt: true, - environments: { + return prisma.project.findMany({ select: { - name: true, - stage: true, - cluster: { - select: { label: true }, - }, + name: true, + description: true, + createdAt: true, + updatedAt: true, + environments: { + select: { + name: true, + stage: true, + cluster: { + select: { label: true }, + }, + }, + }, + owner: true, }, - }, - owner: true, - }, - }) + }); } export function getRolesByProjectId(projectId: Project['id']) { - return prisma.projectRole.findMany({ - where: { projectId }, - }) + return prisma.projectRole.findMany({ + where: { projectId }, + }); } const clusterInfosSelect = { - id: true, - infos: true, - label: true, - external: true, - privacy: true, - secretName: true, - kubeconfig: true, - clusterResources: true, - cpu: true, - gpu: true, - memory: true, - zone: { - select: { - id: true, - slug: true, - argocdUrl: true, - label: true, + id: true, + infos: true, + label: true, + external: true, + privacy: true, + secretName: true, + kubeconfig: true, + clusterResources: true, + cpu: true, + gpu: true, + memory: true, + zone: { + select: { + id: true, + slug: true, + argocdUrl: true, + label: true, + }, }, - }, -} +}; export function getHookProjectInfos(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: { - members: { include: { user: true }, where: { user: { type: 'human' } } }, - clusters: { select: clusterInfosSelect }, - environments: { + return prisma.project.findUniqueOrThrow({ + where: { id }, include: { - stage: true, - cluster: { - select: clusterInfosSelect, - }, + members: { + include: { user: true }, + where: { user: { type: 'human' } }, + }, + clusters: { select: clusterInfosSelect }, + environments: { + include: { + stage: true, + cluster: { + select: clusterInfosSelect, + }, + }, + }, + repositories: true, + plugins: { + select: { + key: true, + pluginName: true, + value: true, + }, + }, + owner: true, + roles: true, }, - }, - repositories: true, - plugins: { - select: { - key: true, - pluginName: true, - value: true, - }, - }, - owner: true, - roles: true, - }, - }) + }); } // CREATE interface CreateProjectParams { - name: Project['name'] - description?: Project['description'] - ownerId: User['id'] - slug: Project['slug'] - limitless: boolean - hprodCpu: number - hprodGpu: number - hprodMemory: number - prodCpu: number - prodGpu: number - prodMemory: number + name: Project['name']; + description?: Project['description']; + ownerId: User['id']; + slug: Project['slug']; + limitless: boolean; + hprodCpu: number; + hprodGpu: number; + hprodMemory: number; + prodCpu: number; + prodGpu: number; + prodMemory: number; } export function initializeProject(params: CreateProjectParams) { - return prisma.project.create({ - data: { - description: params.description ?? '', - status: ProjectStatus.created, - locked: false, - ...params, - }, - }) + return prisma.project.create({ + data: { + description: params.description ?? '', + status: ProjectStatus.created, + locked: false, + ...params, + }, + }); } // UPDATE export function lockProject(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { locked: true }, - }) + return prisma.project.update({ + where: { id }, + data: { locked: true }, + }); } export function updateProjectCreated(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { - status: ProjectStatus.created, - lastSuccessProvisionningVersion: appVersion, - }, - include: baseProjectIncludes, - }) + return prisma.project.update({ + where: { id }, + data: { + status: ProjectStatus.created, + lastSuccessProvisionningVersion: appVersion, + }, + include: baseProjectIncludes, + }); } export function updateProjectFailed(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { status: ProjectStatus.failed }, - include: baseProjectIncludes, - }) + return prisma.project.update({ + where: { id }, + data: { status: ProjectStatus.failed }, + include: baseProjectIncludes, + }); } export function updateProjectWarning(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { status: ProjectStatus.warning }, - include: baseProjectIncludes, - }) + return prisma.project.update({ + where: { id }, + data: { status: ProjectStatus.warning }, + include: baseProjectIncludes, + }); } -export function addUserToProject({ project, user }: { project: Project, user: User }) { - return prisma.projectMembers.create({ - data: { - userId: user.id, - projectId: project.id, - }, - }) +export function addUserToProject({ + project, + user, +}: { + project: Project; + user: User; +}) { + return prisma.projectMembers.create({ + data: { + userId: user.id, + projectId: project.id, + }, + }); } -export function removeUserFromProject({ projectId, userId }: { projectId: Project['id'], userId: User['id'] }) { - return prisma.projectMembers.delete({ - where: { - projectId_userId: { - projectId, - userId, - }, - }, - }) +export function removeUserFromProject({ + projectId, + userId, +}: { + projectId: Project['id']; + userId: User['id']; +}) { + return prisma.projectMembers.delete({ + where: { + projectId_userId: { + projectId, + userId, + }, + }, + }); } export async function archiveProject(id: Project['id']) { - const project = await prisma.project.findUnique({ - where: { id }, - select: { name: true, slug: true }, - }) - return prisma.project.update({ - where: { id }, - data: { - name: `${project?.name}_${Date.now()}_archived`, - slug: `${project?.slug}_${Date.now()}_archived`, - status: ProjectStatus.archived, - locked: true, - }, - include: baseProjectIncludes, - }) + const project = await prisma.project.findUnique({ + where: { id }, + select: { name: true, slug: true }, + }); + return prisma.project.update({ + where: { id }, + data: { + name: `${project?.name}_${Date.now()}_archived`, + slug: `${project?.slug}_${Date.now()}_archived`, + status: ProjectStatus.archived, + locked: true, + }, + include: baseProjectIncludes, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts index bee5f5235..05b7aa643 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts @@ -1,440 +1,661 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ProjectV2 } from '@cpn-console/shared' -import { PROJECT_PERMS, projectContract } from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getProjectMockInfos, getRandomRequestor, getUserMockInfos } from '../../utils/mocks.js' -import { BadRequest400 } from '../../utils/errors.js' -import * as business from './business.js' -import type { UserDetails } from '../../types/index.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMock = vi.spyOn(business, 'listProjects') -const businessCreateMock = vi.spyOn(business, 'createProject') -const businessUpdateMock = vi.spyOn(business, 'updateProject') -const businessDeleteMock = vi.spyOn(business, 'archiveProject') -const businessSyncMock = vi.spyOn(business, 'replayHooks') -const bulkActionProjectMock = vi.spyOn(business, 'bulkActionProject') -const businessGetSecretsMock = vi.spyOn(business, 'getProjectSecrets') -const businessGenerateDataMock = vi.spyOn(business, 'generateProjectsData') +import type { ProjectV2 } from '@cpn-console/shared'; +import { PROJECT_PERMS, projectContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import type { UserDetails } from '../../types/index.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { + getProjectMockInfos, + getRandomRequestor, + getUserMockInfos, +} from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessListMock = vi.spyOn(business, 'listProjects'); +const businessCreateMock = vi.spyOn(business, 'createProject'); +const businessUpdateMock = vi.spyOn(business, 'updateProject'); +const businessDeleteMock = vi.spyOn(business, 'archiveProject'); +const businessSyncMock = vi.spyOn(business, 'replayHooks'); +const bulkActionProjectMock = vi.spyOn(business, 'bulkActionProject'); +const businessGetSecretsMock = vi.spyOn(business, 'getProjectSecrets'); +const businessGenerateDataMock = vi.spyOn(business, 'generateProjectsData'); describe('test projectContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - const projectOwner: ProjectV2['owner'] = { - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - id: faker.string.uuid(), - type: 'human', - } - const projectId = faker.string.uuid() - const project: Omit = { - name: faker.string.alpha({ length: 10, casing: 'lower' }), - slug: faker.string.alpha({ length: 5, casing: 'lower' }), - description: faker.string.alpha({ length: 5 }), - limitless: false, - hprodCpu: faker.number.int({ min: 0, max: 1000 }), - hprodGpu: faker.number.int({ min: 0, max: 1000 }), - hprodMemory: faker.number.int({ min: 0, max: 1000 }), - prodCpu: faker.number.int({ min: 0, max: 1000 }), - prodGpu: faker.number.int({ min: 0, max: 1000 }), - prodMemory: faker.number.int({ min: 0, max: 1000 }), - clusterIds: [], - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - locked: false, - status: 'created', - everyonePerms: '0', - members: [], - owner: projectOwner, - ownerId: projectOwner.id, - roles: [], - lastSuccessProvisionningVersion: null, - } - describe('check unauthorized user on project behaviour', () => { - // UPDATE - it('on Update', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(project) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(404) - expect(response.json()).toEqual({ message: 'Not Found' }) - }) - - it('on Update without enough perms', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(project) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Forbidden' }) - }) - - // REPLAY - it('on replay', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) - .end() - - expect(businessSyncMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(404) - expect(response.json()).toEqual({ message: 'Not Found' }) - }) - - // SECRETS - it('on see secret', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) - .end() - - expect(businessGetSecretsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(404) - expect(response.json()).toEqual({ message: 'Not Found' }) - }) - - // ARCHIVE - it('on archive', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectContract.archiveProject.path.replace(':projectId', projectId)) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(404) - expect(response.json()).toEqual({ message: 'Not Found' }) - }) - }) - describe('listProjects', () => { - it('should return list of projects', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - const projects = [] - businessListMock.mockResolvedValueOnce(projects) - const response = await app.inject() - .get(projectContract.listProjects.path) - .end() - - expect(businessListMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(projects) - expect(response.statusCode).toEqual(200) - }) - it('should return 400 for non-admin with "all" filter', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - const response = await app.inject() - .get(`${projectContract.listProjects.path}?filter=all`) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('createProject', () => { - it('should create and return project for authorized user', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce({ id: projectId, ...project }) - const response = await app.inject() - .post(projectContract.createProject.path) - .body(project) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ id: projectId, ...project }) - expect(response.statusCode).toEqual(201) - }) - - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(projectContract.createProject.path) - .body(project) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('updateProject', () => { - const projectUpdated: Partial = { description: faker.string.alpha({ length: 5 }) } - - it('should update and return project for authorized user', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(projectUpdated) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) - expect(response.statusCode).toEqual(200) - }) - - it('should not update ownerId if not permitted', async () => { - const userDetails = getRandomRequestor() - const projectPerms = getProjectMockInfos({ projectOwnerId: faker.string.uuid(), projectPermissions: PROJECT_PERMS.MANAGE }) - const projectUpdated = { ownerId: faker.string.uuid(), description: faker.lorem.words() } - const user = getUserMockInfos(false, userDetails as UserDetails, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(projectUpdated) - .end() - - expect(businessUpdateMock).toHaveBeenCalledWith({ description: projectUpdated.description }, projectId, user.user, expect.any(String)) - expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) - expect(response.statusCode).toEqual(200) - }) - - it('should update ownerId and return project', async () => { - const requestor = getRandomRequestor() - const projectPerms = getProjectMockInfos({ projectOwnerId: requestor.id, projectPermissions: PROJECT_PERMS.MANAGE }) - const projectUpdated = { ownerId: faker.string.uuid(), description: faker.lorem.words() } - const user = getUserMockInfos(false, requestor as UserDetails, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(projectUpdated) - .end() - - expect(businessUpdateMock).toHaveBeenCalledWith(projectUpdated, projectId, user.user, expect.any(String)) - expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) - expect(response.statusCode).toEqual(200) - }) - - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(project) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(1) - expect(response.statusCode).toEqual(400) - }) - }) - - describe('archiveProject', () => { - it('should archive project for authorized user', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(1) - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return projects data for admin', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getProjectSecrets', () => { - it('should return project secrets for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const secrets = {} - businessGetSecretsMock.mockResolvedValueOnce(secrets) - const response = await app.inject() - .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) - .end() - - expect(businessGetSecretsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(secrets) - expect(response.statusCode).toEqual(200) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetSecretsMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for unauthorized access to secrets', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('replayHooksForProject', () => { - it('should replay hooks for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(null) - const response = await app.inject() - .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) - .end() - - expect(businessSyncMock).toHaveBeenCalledTimes(1) - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for unauthorized access to replay hooks', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - const response = await app.inject() - .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getProjectsData', () => { - it('should return projects data for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - const data = '' - businessGenerateDataMock.mockResolvedValueOnce(data) - const response = await app.inject() - .get(projectContract.getProjectsData.path) - .end() - - expect(businessGenerateDataMock).toHaveBeenCalledTimes(1) - expect(response.body).toEqual(data) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for non-admin user', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectContract.getProjectsData.path) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('bulkActionProject', () => { - it('should executebulk for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(null) - const response = await app.inject() - .post(projectContract.bulkActionProject.path) - .body({ action: 'lock', projectIds: [projectId] }) - .end() - - expect(response.json()).toBeNull() - expect(bulkActionProjectMock).toHaveBeenCalledTimes(1) - expect(response.statusCode).toEqual(202) - }) - - it('should return 403 for unauthorized access to bulk update', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - const response = await app.inject() - .post(projectContract.bulkActionProject.path) - .body({ action: 'lock', projectIds: [projectId] }) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + const projectOwner: ProjectV2['owner'] = { + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + id: faker.string.uuid(), + type: 'human', + }; + const projectId = faker.string.uuid(); + const project: Omit = { + name: faker.string.alpha({ length: 10, casing: 'lower' }), + slug: faker.string.alpha({ length: 5, casing: 'lower' }), + description: faker.string.alpha({ length: 5 }), + limitless: false, + hprodCpu: faker.number.int({ min: 0, max: 1000 }), + hprodGpu: faker.number.int({ min: 0, max: 1000 }), + hprodMemory: faker.number.int({ min: 0, max: 1000 }), + prodCpu: faker.number.int({ min: 0, max: 1000 }), + prodGpu: faker.number.int({ min: 0, max: 1000 }), + prodMemory: faker.number.int({ min: 0, max: 1000 }), + clusterIds: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + locked: false, + status: 'created', + everyonePerms: '0', + members: [], + owner: projectOwner, + ownerId: projectOwner.id, + roles: [], + lastSuccessProvisionningVersion: null, + }; + describe('check unauthorized user on project behaviour', () => { + // UPDATE + it('on Update', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + projectContract.updateProject.path.replace( + ':projectId', + projectId, + ), + ) + .body(project) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(404); + expect(response.json()).toEqual({ message: 'Not Found' }); + }); + + it('on Update without enough perms', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + projectContract.updateProject.path.replace( + ':projectId', + projectId, + ), + ) + .body(project) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ message: 'Forbidden' }); + }); + + // REPLAY + it('on replay', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + projectContract.replayHooksForProject.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(businessSyncMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(404); + expect(response.json()).toEqual({ message: 'Not Found' }); + }); + + // SECRETS + it('on see secret', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + projectContract.getProjectSecrets.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(businessGetSecretsMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(404); + expect(response.json()).toEqual({ message: 'Not Found' }); + }); + + // ARCHIVE + it('on archive', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + projectContract.archiveProject.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(businessDeleteMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(404); + expect(response.json()).toEqual({ message: 'Not Found' }); + }); + }); + describe('listProjects', () => { + it('should return list of projects', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + const projects = []; + businessListMock.mockResolvedValueOnce(projects); + const response = await app + .inject() + .get(projectContract.listProjects.path) + .end(); + + expect(businessListMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(projects); + expect(response.statusCode).toEqual(200); + }); + it('should return 400 for non-admin with "all" filter', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + const response = await app + .inject() + .get(`${projectContract.listProjects.path}?filter=all`) + .end(); + + expect(response.statusCode).toEqual(400); + }); + }); + + describe('createProject', () => { + it('should create and return project for authorized user', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce({ + id: projectId, + ...project, + }); + const response = await app + .inject() + .post(projectContract.createProject.path) + .body(project) + .end(); + + expect(businessCreateMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual({ id: projectId, ...project }); + expect(response.statusCode).toEqual(201); + }); + + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .post(projectContract.createProject.path) + .body(project) + .end(); + + expect(response.statusCode).toEqual(400); + }); + }); + + describe('updateProject', () => { + const projectUpdated: Partial = { + description: faker.string.alpha({ length: 5 }), + }; + + it('should update and return project for authorized user', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce({ + id: projectId, + ...project, + ...projectUpdated, + }); + const response = await app + .inject() + .put( + projectContract.updateProject.path.replace( + ':projectId', + projectId, + ), + ) + .body(projectUpdated) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual({ + id: projectId, + ...project, + ...projectUpdated, + }); + expect(response.statusCode).toEqual(200); + }); + + it('should not update ownerId if not permitted', async () => { + const userDetails = getRandomRequestor(); + const projectPerms = getProjectMockInfos({ + projectOwnerId: faker.string.uuid(), + projectPermissions: PROJECT_PERMS.MANAGE, + }); + const projectUpdated = { + ownerId: faker.string.uuid(), + description: faker.lorem.words(), + }; + const user = getUserMockInfos( + false, + userDetails as UserDetails, + projectPerms, + ); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce({ + id: projectId, + ...project, + ...projectUpdated, + }); + const response = await app + .inject() + .put( + projectContract.updateProject.path.replace( + ':projectId', + projectId, + ), + ) + .body(projectUpdated) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledWith( + { description: projectUpdated.description }, + projectId, + user.user, + expect.any(String), + ); + expect(response.json()).toEqual({ + id: projectId, + ...project, + ...projectUpdated, + }); + expect(response.statusCode).toEqual(200); + }); + + it('should update ownerId and return project', async () => { + const requestor = getRandomRequestor(); + const projectPerms = getProjectMockInfos({ + projectOwnerId: requestor.id, + projectPermissions: PROJECT_PERMS.MANAGE, + }); + const projectUpdated = { + ownerId: faker.string.uuid(), + description: faker.lorem.words(), + }; + const user = getUserMockInfos( + false, + requestor as UserDetails, + projectPerms, + ); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce({ + id: projectId, + ...project, + ...projectUpdated, + }); + const response = await app + .inject() + .put( + projectContract.updateProject.path.replace( + ':projectId', + projectId, + ), + ) + .body(projectUpdated) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledWith( + projectUpdated, + projectId, + user.user, + expect.any(String), + ); + expect(response.json()).toEqual({ + id: projectId, + ...project, + ...projectUpdated, + }); + expect(response.statusCode).toEqual(200); + }); + + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .put( + projectContract.updateProject.path.replace( + ':projectId', + projectId, + ), + ) + .body(project) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledTimes(1); + expect(response.statusCode).toEqual(400); + }); + }); + + describe('archiveProject', () => { + it('should archive project for authorized user', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce(null); + const response = await app + .inject() + .delete( + projectContract.archiveProject.path.replace( + ':projectId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessDeleteMock).toHaveBeenCalledTimes(1); + expect(response.body).toBeFalsy(); + expect(response.statusCode).toEqual(204); + }); + + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .delete( + projectContract.archiveProject.path.replace( + ':projectId', + faker.string.uuid(), + ), + ) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return projects data for admin', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + projectContract.archiveProject.path.replace( + ':projectId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessDeleteMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('getProjectSecrets', () => { + it('should return project secrets for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.SEE_SECRETS, + }); + const user = getUserMockInfos(true, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const secrets = {}; + businessGetSecretsMock.mockResolvedValueOnce(secrets); + const response = await app + .inject() + .get( + projectContract.getProjectSecrets.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(businessGetSecretsMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(secrets); + expect(response.statusCode).toEqual(200); + }); + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE, + }); + const user = getUserMockInfos(true, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessGetSecretsMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .get( + projectContract.getProjectSecrets.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for unauthorized access to secrets', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + projectContract.getProjectSecrets.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(403); + }); + }); + + describe('replayHooksForProject', () => { + it('should replay hooks for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE, + }); + const user = getUserMockInfos(true, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessSyncMock.mockResolvedValueOnce(null); + const response = await app + .inject() + .put( + projectContract.replayHooksForProject.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(businessSyncMock).toHaveBeenCalledTimes(1); + expect(response.body).toBeFalsy(); + expect(response.statusCode).toEqual(204); + }); + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE, + }); + const user = getUserMockInfos(true, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessSyncMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .put( + projectContract.replayHooksForProject.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for unauthorized access to replay hooks', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + const response = await app + .inject() + .put( + projectContract.replayHooksForProject.path.replace( + ':projectId', + projectId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(403); + }); + }); + + describe('getProjectsData', () => { + it('should return projects data for admin', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + const data = ''; + businessGenerateDataMock.mockResolvedValueOnce(data); + const response = await app + .inject() + .get(projectContract.getProjectsData.path) + .end(); + + expect(businessGenerateDataMock).toHaveBeenCalledTimes(1); + expect(response.body).toEqual(data); + expect(response.statusCode).toEqual(200); + }); + + it('should return 403 for non-admin user', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(projectContract.getProjectsData.path) + .end(); + + expect(response.statusCode).toEqual(403); + }); + }); + + describe('bulkActionProject', () => { + it('should executebulk for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE, + }); + const user = getUserMockInfos(true, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessSyncMock.mockResolvedValueOnce(null); + const response = await app + .inject() + .post(projectContract.bulkActionProject.path) + .body({ action: 'lock', projectIds: [projectId] }) + .end(); + + expect(response.json()).toBeNull(); + expect(bulkActionProjectMock).toHaveBeenCalledTimes(1); + expect(response.statusCode).toEqual(202); + }); + + it('should return 403 for unauthorized access to bulk update', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + const response = await app + .inject() + .post(projectContract.bulkActionProject.path) + .body({ action: 'lock', projectIds: [projectId] }) + .end(); + + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts index ecf13df98..dea4256e1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts @@ -1,199 +1,230 @@ -import type { AsyncReturnType } from '@cpn-console/shared' -import { AdminAuthorized, ProjectAuthorized, projectContract } from '@cpn-console/shared' +import type { AsyncReturnType } from '@cpn-console/shared'; import { - archiveProject, - bulkActionProject, - createProject, - generateProjectsData, - getProject, - getProjectSecrets, - listProjects, - replayHooks, - updateProject, -} from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors.js' + AdminAuthorized, + ProjectAuthorized, + projectContract, +} from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { + BadRequest400, + ErrorResType, + Forbidden403, + NotFound404, + Unauthorized401, +} from '@old-server/utils/errors.js'; + +import { + archiveProject, + bulkActionProject, + createProject, + generateProjectsData, + getProject, + getProjectSecrets, + listProjects, + replayHooks, + updateProject, +} from './business.js'; export function projectRouter() { - return serverInstance.router(projectContract, { - - // Récupérer des projets - listProjects: async ({ request: req, query }) => { - const { adminPermissions, user } = await authUser(req) - let body: AsyncReturnType = [] - - if (adminPermissions && !user) { // c'est donc un compte de service - query.filter = 'all' - } - if (query.filter === 'all' && !AdminAuthorized.isAdmin(adminPermissions)) { - return new BadRequest400('Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre \'all\'') - } - - body = await listProjects( - query, - user?.id, - ) - - return { - status: 200, - body, - } - }, - - // Récupérer les secrets d'un projet - getProjectSecrets: async ({ request: req, params }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.SeeSecrets(perms)) return new Forbidden403() - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const body = await getProjectSecrets(projectId) - - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - // Créer un projet - createProject: async ({ request: req, body: data }) => { - const perms = await authUser(req) - if (perms.user?.type !== 'human') return new Unauthorized401('Cannot find requestor in database') - const body = await createProject(data, perms.user, req.id) - - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - // Récuperer un seul projet - getProject: async ({ request: req, params }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - if (!perms.projectId) return new NotFound404() - if (!isAdmin) { - if (!perms.projectPermissions) { - return new NotFound404() - } - if (perms.projectStatus === 'archived') { - return new NotFound404() - } - } - - const body = await getProject(projectId) - - return { - status: 200, - body, - } - }, - - // Mettre à jour un projet - updateProject: async ({ request: req, params, body: data }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - - if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - const isOwner = perms.projectOwnerId === perms.user.id - - if (!perms.projectPermissions && !isAdmin) return new NotFound404() - if (!isAdmin) { // filtrage des clés par niveau de permissions - delete data.locked - if (!isOwner) { - delete data.ownerId // impossible de toucher à cette clé - } - } - if (perms.projectLocked) { - if (!isAdmin) return new Forbidden403('Le projet est verrouillé') - if (data.locked !== false) return new Forbidden403('Veuillez déverrouiler le projet pour le mettre à jour') - } - - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() - - const body = await updateProject(data, projectId, perms.user, req.id) - - if (body instanceof ErrorResType) return body - return { - status: 200, - body, - } - }, - - // Reprovisionner un projet - replayHooksForProject: async ({ request: req, params }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - if (!perms.projectPermissions && !isAdmin) return new NotFound404() - if (!ProjectAuthorized.ReplayHooks(perms)) return new Forbidden403() - - const body = await replayHooks({ - projectId, - userId: perms.user?.id, - requestId: req.id, - }) - - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - - // Archiver un projet - archiveProject: async ({ request: req, params }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - if (!perms.projectPermissions && !isAdmin) return new NotFound404() - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() - - const body = await archiveProject(projectId, perms.user, req.id) - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - // Récupérer les données de tous les projets pour export - getProjectsData: async ({ request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const body = await generateProjectsData() - - return { - status: 200, - body, - } - }, - - bulkActionProject: async ({ request: req, body }) => { - const perms = await authUser(req) - - if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - await bulkActionProject(body, perms.user, req.id) - - return { - status: 202, - body: null, - } - }, - }) + return serverInstance.router(projectContract, { + // Récupérer des projets + listProjects: async ({ request: req, query }) => { + const { adminPermissions, user } = await authUser(req); + let body: AsyncReturnType = []; + + if (adminPermissions && !user) { + // c'est donc un compte de service + query.filter = 'all'; + } + if ( + query.filter === 'all' && + !AdminAuthorized.isAdmin(adminPermissions) + ) { + return new BadRequest400( + "Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre 'all'", + ); + } + + body = await listProjects(query, user?.id); + + return { + status: 200, + body, + }; + }, + + // Récupérer les secrets d'un projet + getProjectSecrets: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.SeeSecrets(perms)) return new Forbidden403(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await getProjectSecrets(projectId); + + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Créer un projet + createProject: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (perms.user?.type !== 'human') + return new Unauthorized401('Cannot find requestor in database'); + const body = await createProject(data, perms.user, req.id); + + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Récuperer un seul projet + getProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.projectId) return new NotFound404(); + if (!isAdmin) { + if (!perms.projectPermissions) { + return new NotFound404(); + } + if (perms.projectStatus === 'archived') { + return new NotFound404(); + } + } + + const body = await getProject(projectId); + + return { + status: 200, + body, + }; + }, + + // Mettre à jour un projet + updateProject: async ({ request: req, params, body: data }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401('Cannot find requestor in database'); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + const isOwner = perms.projectOwnerId === perms.user.id; + + if (!perms.projectPermissions && !isAdmin) return new NotFound404(); + if (!isAdmin) { + // filtrage des clés par niveau de permissions + delete data.locked; + if (!isOwner) { + delete data.ownerId; // impossible de toucher à cette clé + } + } + if (perms.projectLocked) { + if (!isAdmin) + return new Forbidden403('Le projet est verrouillé'); + if (data.locked !== false) + return new Forbidden403( + 'Veuillez déverrouiler le projet pour le mettre à jour', + ); + } + + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); + + const body = await updateProject( + data, + projectId, + perms.user, + req.id, + ); + + if (body instanceof ErrorResType) return body; + return { + status: 200, + body, + }; + }, + + // Reprovisionner un projet + replayHooksForProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.projectPermissions && !isAdmin) return new NotFound404(); + if (!ProjectAuthorized.ReplayHooks(perms)) + return new Forbidden403(); + + const body = await replayHooks({ + projectId, + userId: perms.user?.id, + requestId: req.id, + }); + + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + + // Archiver un projet + archiveProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.user) + return new Unauthorized401('Cannot find requestor in database'); + if (!perms.projectPermissions && !isAdmin) return new NotFound404(); + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); + + const body = await archiveProject(projectId, perms.user, req.id); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + // Récupérer les données de tous les projets pour export + getProjectsData: async ({ request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await generateProjectsData(); + + return { + status: 200, + body, + }; + }, + + bulkActionProject: async ({ request: req, body }) => { + const perms = await authUser(req); + + if (!perms.user) + return new Unauthorized401('Cannot find requestor in database'); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + await bulkActionProject(body, perms.user, req.id); + + return { + status: 202, + body: null, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts index 909657a6d..4d17aa435 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts @@ -1,14 +1,14 @@ -export * from '@old-server/resources/admin-role/queries.js' -export * from '@old-server/resources/cluster/queries.js' -export * from '@old-server/resources/service-chain/queries.js' -export * from '@old-server/resources/environment/queries.js' -export * from '@old-server/resources/log/queries.js' -export * from '@old-server/resources/project/queries.js' -export * from '@old-server/resources/project-member/queries.js' -export * from '@old-server/resources/project-role/queries.js' -export * from '@old-server/resources/project-service/queries.js' -export * from '@old-server/resources/repository/queries.js' -export * from '@old-server/resources/user/queries.js' -export * from '@old-server/resources/stage/queries.js' -export * from '@old-server/resources/zone/queries.js' -export * from '@old-server/resources/system/settings/queries.js' +export * from '@old-server/resources/admin-role/queries.js'; +export * from '@old-server/resources/cluster/queries.js'; +export * from '@old-server/resources/service-chain/queries.js'; +export * from '@old-server/resources/environment/queries.js'; +export * from '@old-server/resources/log/queries.js'; +export * from '@old-server/resources/project/queries.js'; +export * from '@old-server/resources/project-member/queries.js'; +export * from '@old-server/resources/project-role/queries.js'; +export * from '@old-server/resources/project-service/queries.js'; +export * from '@old-server/resources/repository/queries.js'; +export * from '@old-server/resources/user/queries.js'; +export * from '@old-server/resources/stage/queries.js'; +export * from '@old-server/resources/zone/queries.js'; +export * from '@old-server/resources/system/settings/queries.js'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts index 31d8ad813..1f20356c5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts @@ -1,115 +1,179 @@ -import type { Project, Repository, User } from '@prisma/client' -import type { CreateRepositoryBody, UpdateRepositoryBody } from '@cpn-console/shared' -import { addLogs, deleteRepository as deleteRepositoryQuery, getProjectInfosAndRepos, getProjectRepositories as getProjectRepositoriesQuery, initializeRepository, updateRepository as updateRepositoryQuery } from '@old-server/resources/queries-index.js' -import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors.js' -import { hook } from '@old-server/utils/hook-wrapper.js' +import type { + CreateRepositoryBody, + UpdateRepositoryBody, +} from '@cpn-console/shared'; +import { + addLogs, + deleteRepository as deleteRepositoryQuery, + getProjectInfosAndRepos, + getProjectRepositories as getProjectRepositoriesQuery, + initializeRepository, + updateRepository as updateRepositoryQuery, +} from '@old-server/resources/queries-index.js'; +import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors.js'; +import { hook } from '@old-server/utils/hook-wrapper.js'; +import type { Project, Repository, User } from '@prisma/client'; export async function getProjectRepositories(projectId: Project['id']) { - return getProjectRepositoriesQuery(projectId) + return getProjectRepositoriesQuery(projectId); } export async function syncRepository({ - repositoryId, - userId, - syncAllBranches, - branchName, - requestId, + repositoryId, + userId, + syncAllBranches, + branchName, + requestId, }: { - repositoryId: Repository['id'] - userId: User['id'] - syncAllBranches: boolean - branchName?: string - requestId: string + repositoryId: Repository['id']; + userId: User['id']; + syncAllBranches: boolean; + branchName?: string; + requestId: string; }) { - const hookReply = await hook.misc.syncRepository(repositoryId, { syncAllBranches, branchName }) - await addLogs({ action: 'Sync Repository', data: hookReply, userId, requestId, projectId: hookReply.args.id }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la synchronisation du dépôt') - } - return null + const hookReply = await hook.misc.syncRepository(repositoryId, { + syncAllBranches, + branchName, + }); + await addLogs({ + action: 'Sync Repository', + data: hookReply, + userId, + requestId, + projectId: hookReply.args.id, + }); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services à la synchronisation du dépôt', + ); + } + return null; } export async function createRepository({ - data, - userId, - requestId, + data, + userId, + requestId, }: { - data: CreateRepositoryBody - userId: User['id'] - requestId: string + data: CreateRepositoryBody; + userId: User['id']; + requestId: string; }) { - const project = await getProjectInfosAndRepos(data.projectId) + const project = await getProjectInfosAndRepos(data.projectId); - if (project.repositories?.find(repo => repo.internalRepoName === data.internalRepoName)) return new BadRequest400(`Le nom du dépôt interne ${data.internalRepoName} existe déjà en base pour ce projet`) - const dbData = { ...data, isInfra: !!data.isInfra, isPrivate: !!data.isPrivate } - delete dbData.externalToken + if ( + project.repositories?.find( + (repo) => repo.internalRepoName === data.internalRepoName, + ) + ) + return new BadRequest400( + `Le nom du dépôt interne ${data.internalRepoName} existe déjà en base pour ce projet`, + ); + const dbData = { + ...data, + isInfra: !!data.isInfra, + isPrivate: !!data.isPrivate, + }; + delete dbData.externalToken; - const repo = await initializeRepository(dbData) - const { results } = await hook.project.upsert(project.id, data.isPrivate - ? { - [repo.internalRepoName]: { - token: data.externalToken ?? '', - username: data.externalUserName ?? '', - }, - } - : undefined) - await addLogs({ action: 'Create Repository', data: results, userId, requestId, projectId: repo.projectId }) - if (results.failed) { - return new Unprocessable422('Echec des services lors de la création du dépôt') - } + const repo = await initializeRepository(dbData); + const { results } = await hook.project.upsert( + project.id, + data.isPrivate + ? { + [repo.internalRepoName]: { + token: data.externalToken ?? '', + username: data.externalUserName ?? '', + }, + } + : undefined, + ); + await addLogs({ + action: 'Create Repository', + data: results, + userId, + requestId, + projectId: repo.projectId, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services lors de la création du dépôt', + ); + } - if (data.externalRepoUrl) { - await syncRepository({ repositoryId: repo.id, requestId, syncAllBranches: true, userId }) - } - return repo + if (data.externalRepoUrl) { + await syncRepository({ + repositoryId: repo.id, + requestId, + syncAllBranches: true, + userId, + }); + } + return repo; } export async function updateRepository({ - repositoryId, - data, - userId, - requestId, + repositoryId, + data, + userId, + requestId, }: { - repositoryId: Repository['id'] - data: Partial - userId: User['id'] - requestId: string + repositoryId: Repository['id']; + data: Partial; + userId: User['id']; + requestId: string; }) { - const dbData = { ...data } - delete dbData.externalToken - const repo = await updateRepositoryQuery(repositoryId, dbData) + const dbData = { ...data }; + delete dbData.externalToken; + const repo = await updateRepositoryQuery(repositoryId, dbData); - const { results } = await hook.project.upsert(repo.projectId, { - [repo.internalRepoName]: { - username: repo.externalUserName ?? '', - token: data.externalToken ?? '', - }, - }) - await addLogs({ action: 'Update Repository', data: results, userId, requestId, projectId: repo.projectId }) - if (results.failed) { - return new Unprocessable422('Echec des services à la mise à jour du dépôt') - } + const { results } = await hook.project.upsert(repo.projectId, { + [repo.internalRepoName]: { + username: repo.externalUserName ?? '', + token: data.externalToken ?? '', + }, + }); + await addLogs({ + action: 'Update Repository', + data: results, + userId, + requestId, + projectId: repo.projectId, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services à la mise à jour du dépôt', + ); + } - return repo + return repo; } export async function deleteRepository({ - repositoryId, - userId, - requestId, - projectId, + repositoryId, + userId, + requestId, + projectId, }: { - repositoryId: Repository['id'] - userId: User['id'] - requestId: string - projectId: Project['id'] + repositoryId: Repository['id']; + userId: User['id']; + requestId: string; + projectId: Project['id']; }) { - await deleteRepositoryQuery(repositoryId) + await deleteRepositoryQuery(repositoryId); - const { results } = await hook.project.upsert(projectId) - await addLogs({ action: 'Delete Repository', data: results, userId, requestId, projectId }) - if (results.failed) { - return new Unprocessable422('Echec des services à la suppression du dépôt') - } - return null + const { results } = await hook.project.upsert(projectId); + await addLogs({ + action: 'Delete Repository', + data: results, + userId, + requestId, + projectId, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services à la suppression du dépôt', + ); + } + return null; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts index f4ee871fe..95bd00582 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts @@ -1,59 +1,81 @@ -import type { Project, Repository } from '@prisma/client' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { Project, Repository } from '@prisma/client'; // SELECT export function getRepositoryById(id: Repository['id']) { - return prisma.repository.findUniqueOrThrow({ where: { id } }) + return prisma.repository.findUniqueOrThrow({ where: { id } }); } export function getProjectRepositories(projectId: Project['id']) { - return prisma.repository.findMany({ where: { projectId } }) + return prisma.repository.findMany({ where: { projectId } }); } // CREATE -type RepositoryCreate = Pick & - Partial> - -export function initializeRepository({ projectId, internalRepoName, externalRepoUrl, isInfra, isPrivate, externalUserName }: RepositoryCreate) { - return prisma.repository.create({ - data: { - projectId, - internalRepoName, - externalRepoUrl, - externalUserName, - isInfra, - isPrivate, - }, - }) +type RepositoryCreate = Pick< + Repository, + 'projectId' | 'internalRepoName' | 'isInfra' | 'isPrivate' +> & + Partial>; + +export function initializeRepository({ + projectId, + internalRepoName, + externalRepoUrl, + isInfra, + isPrivate, + externalUserName, +}: RepositoryCreate) { + return prisma.repository.create({ + data: { + projectId, + internalRepoName, + externalRepoUrl, + externalUserName, + isInfra, + isPrivate, + }, + }); } export function getHookRepository(id: Repository['id']) { - return prisma.repository.findUniqueOrThrow({ - where: { - id, - }, - include: { - project: true, - }, - }) + return prisma.repository.findUniqueOrThrow({ + where: { + id, + }, + include: { + project: true, + }, + }); } // UPDATE -export function updateRepository(id: Repository['id'], infos: Partial) { - return prisma.repository.update({ where: { id }, data: { ...infos } }) +export function updateRepository( + id: Repository['id'], + infos: Partial, +) { + return prisma.repository.update({ where: { id }, data: { ...infos } }); } // DELETE export async function deleteRepository(id: Repository['id']) { - const doesRepoExist = await getRepositoryById(id) - if (!doesRepoExist) throw new Error('Le dépôt interne demandé n\'existe pas en base pour ce projet') - return prisma.repository.delete({ where: { id } }) + const doesRepoExist = await getRepositoryById(id); + if (!doesRepoExist) + throw new Error( + "Le dépôt interne demandé n'existe pas en base pour ce projet", + ); + return prisma.repository.delete({ where: { id } }); } export function deleteAllRepositoryForProject(id: Project['id']) { - return prisma.repository.deleteMany({ where: { projectId: id } }) + return prisma.repository.deleteMany({ where: { projectId: id } }); } -export function _createRepository(data: Parameters[0]['create']) { - return prisma.repository.upsert({ create: data, update: data, where: { id: data.id } }) +export function _createRepository( + data: Parameters[0]['create'], +) { + return prisma.repository.upsert({ + create: data, + update: data, + where: { id: data.id }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts index a08f384e3..7e025ed47 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts @@ -1,402 +1,646 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { PROJECT_PERMS, repositoryContract } from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { atDates, getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js' -import { BadRequest400 } from '../../utils/errors.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessCreateMock = vi.spyOn(business, 'createRepository') -const businessUpdateMock = vi.spyOn(business, 'updateRepository') -const businessDeleteMock = vi.spyOn(business, 'deleteRepository') -const businessSyncMock = vi.spyOn(business, 'syncRepository') -const businessGetProjectRepositoriesMock = vi.spyOn(business, 'getProjectRepositories') +import { PROJECT_PERMS, repositoryContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { + atDates, + getProjectMockInfos, + getUserMockInfos, +} from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessCreateMock = vi.spyOn(business, 'createRepository'); +const businessUpdateMock = vi.spyOn(business, 'updateRepository'); +const businessDeleteMock = vi.spyOn(business, 'deleteRepository'); +const businessSyncMock = vi.spyOn(business, 'syncRepository'); +const businessGetProjectRepositoriesMock = vi.spyOn( + business, + 'getProjectRepositories', +); describe('repositoryRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const projectId = faker.string.uuid() - const repositoryId = faker.string.uuid() - const repositoryData = { - projectId, - externalRepoUrl: `${faker.internet.url()}.git`, - isPrivate: true, - externalToken: faker.string.alpha(), - externalUserName: faker.internet.username(), - isInfra: false, - internalRepoName: faker.string.alpha({ length: 5, casing: 'lower' }), - } - - describe('listRepositories', () => { - it('should return repositories for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetProjectRepositoriesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(repositoryContract.listRepositories.path) - .query({ projectId }) - .end() - - expect(businessGetProjectRepositoriesMock).toHaveBeenCalledWith(projectId) - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should return empty for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.REPLAY_HOOKS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(repositoryContract.listRepositories.path) - .query({ projectId }) - .end() - - expect(businessGetProjectRepositoriesMock).toHaveBeenCalledTimes(0) - expect(response.json()).toEqual([]) - }) - }) - - describe('syncRepository', () => { - it('should synchronize repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(204) - expect(businessSyncMock).toHaveBeenCalledWith({ repositoryId, userId: user.user.id, branchName: 'main', requestId: expect.any(String), syncAllBranches: false }) - }) - - it('should return 403 for forbidden sync attempt', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('createRepository', () => { - it('should create repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...atDates }) - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(201) - expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData }) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(404) - }) - it('should return 403 for insuficient permissions', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(403) - }) - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('updateRepository', () => { - const repoUpdateData = { isInfra: true } - it('should update repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...repoUpdateData, ...atDates }) - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(200) - expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData, ...repoUpdateData }) - }) - - it('should update repository and drop creds if is not private', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const repoUpdateData = { isPrivate: false, externalUserName: 'test' } - businessUpdateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...repoUpdateData, ...atDates }) - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(businessUpdateMock).toHaveBeenCalledWith({ data: { isPrivate: false }, repositoryId, requestId: expect.any(String), userId: user.user.id }) - expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData, ...repoUpdateData }) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not enough permissions', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(400) - }) - // TODO add tests about filtering - }) - - describe('deleteRepository', () => { - it('should delete repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - it('should return 403 if not enough privilege', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + const projectId = faker.string.uuid(); + const repositoryId = faker.string.uuid(); + const repositoryData = { + projectId, + externalRepoUrl: `${faker.internet.url()}.git`, + isPrivate: true, + externalToken: faker.string.alpha(), + externalUserName: faker.internet.username(), + isInfra: false, + internalRepoName: faker.string.alpha({ length: 5, casing: 'lower' }), + }; + + describe('listRepositories', () => { + it('should return repositories for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessGetProjectRepositoriesMock.mockResolvedValueOnce([]); + + const response = await app + .inject() + .get(repositoryContract.listRepositories.path) + .query({ projectId }) + .end(); + + expect(businessGetProjectRepositoriesMock).toHaveBeenCalledWith( + projectId, + ); + expect(response.json()).toEqual([]); + expect(response.statusCode).toEqual(200); + }); + + it('should return empty for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.REPLAY_HOOKS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(repositoryContract.listRepositories.path) + .query({ projectId }) + .end(); + + expect(businessGetProjectRepositoriesMock).toHaveBeenCalledTimes(0); + expect(response.json()).toEqual([]); + }); + }); + + describe('syncRepository', () => { + it('should synchronize repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessSyncMock.mockResolvedValueOnce(null); + + const response = await app + .inject() + .post( + repositoryContract.syncRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body({ branchName: 'main', syncAllBranches: false }) + .end(); + + expect(response.statusCode).toEqual(204); + expect(businessSyncMock).toHaveBeenCalledWith({ + repositoryId, + userId: user.user.id, + branchName: 'main', + requestId: expect.any(String), + syncAllBranches: false, + }); + }); + + it('should return 403 for forbidden sync attempt', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.SEE_SECRETS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + repositoryContract.syncRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body({ branchName: 'main', syncAllBranches: false }) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + repositoryContract.syncRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body({ branchName: 'main', syncAllBranches: false }) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post( + repositoryContract.syncRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body({ branchName: 'main', syncAllBranches: false }) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessSyncMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .post( + repositoryContract.syncRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body({ branchName: 'main', syncAllBranches: false }) + .end(); + + expect(response.statusCode).toEqual(400); + }); + }); + + describe('createRepository', () => { + it('should create repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce({ + id: repositoryId, + ...repositoryData, + ...atDates, + }); + const response = await app + .inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end(); + + expect(response.statusCode).toEqual(201); + expect(response.json()).toMatchObject({ + id: repositoryId, + ...repositoryData, + }); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end(); + + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + expect(response.statusCode).toEqual(403); + }); + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end(); + + expect(response.statusCode).toEqual(404); + }); + it('should return 403 for insuficient permissions', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end(); + + expect(response.statusCode).toEqual(403); + }); + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end(); + + expect(response.statusCode).toEqual(400); + }); + }); + + describe('updateRepository', () => { + const repoUpdateData = { isInfra: true }; + it('should update repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce({ + id: repositoryId, + ...repositoryData, + ...repoUpdateData, + ...atDates, + }); + const response = await app + .inject() + .put( + repositoryContract.updateRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body(repoUpdateData) + .end(); + + expect(response.statusCode).toEqual(200); + expect(response.json()).toMatchObject({ + id: repositoryId, + ...repositoryData, + ...repoUpdateData, + }); + }); + + it('should update repository and drop creds if is not private', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const repoUpdateData = { + isPrivate: false, + externalUserName: 'test', + }; + businessUpdateMock.mockResolvedValueOnce({ + id: repositoryId, + ...repositoryData, + ...repoUpdateData, + ...atDates, + }); + const response = await app + .inject() + .put( + repositoryContract.updateRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body(repoUpdateData) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledWith({ + data: { isPrivate: false }, + repositoryId, + requestId: expect.any(String), + userId: user.user.id, + }); + expect(response.json()).toMatchObject({ + id: repositoryId, + ...repositoryData, + ...repoUpdateData, + }); + expect(response.statusCode).toEqual(200); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectLocked: true }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + repositoryContract.updateRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body(repoUpdateData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if not enough permissions', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + repositoryContract.updateRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body(repoUpdateData) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + repositoryContract.updateRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body(repoUpdateData) + .end(); + + expect(response.statusCode).toEqual(404); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + repositoryContract.updateRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body(repoUpdateData) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + }); + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .put( + repositoryContract.updateRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .body(repoUpdateData) + .end(); + + expect(response.statusCode).toEqual(400); + }); + // TODO add tests about filtering + }); + + describe('deleteRepository', () => { + it('should delete repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce(null); + const response = await app + .inject() + .delete( + repositoryContract.deleteRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(204); + }); + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + projectLocked: true, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + repositoryContract.deleteRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(403); + expect(response.json()).toEqual({ + message: 'Le projet est verrouillé', + }); + }); + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + projectStatus: 'archived', + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + repositoryContract.deleteRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .end(); + + expect(response.json()).toEqual({ + message: 'Le projet est archivé', + }); + expect(response.statusCode).toEqual(403); + }); + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: 0n, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + repositoryContract.deleteRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(404); + }); + it('should return 403 if not enough privilege', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + repositoryContract.deleteRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(403); + }); + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ + projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, + }); + const user = getUserMockInfos(false, undefined, projectPerms); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .delete( + repositoryContract.deleteRepository.path.replace( + ':repositoryId', + repositoryId, + ), + ) + .end(); + + expect(response.statusCode).toEqual(400); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts index 02b9c14db..70352d135 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts @@ -1,135 +1,191 @@ -import { AdminAuthorized, ProjectAuthorized, fakeToken, repositoryContract } from '@cpn-console/shared' import { - createRepository, - deleteRepository, - getProjectRepositories, - syncRepository, - updateRepository, -} from './business.js' -import { serverInstance } from '@old-server/app.js' - -import { filterObjectByKeys } from '@old-server/utils/queries-tools.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors.js' + AdminAuthorized, + ProjectAuthorized, + fakeToken, + repositoryContract, +} from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { + ErrorResType, + Forbidden403, + NotFound404, + Unauthorized401, +} from '@old-server/utils/errors.js'; +import { filterObjectByKeys } from '@old-server/utils/queries-tools.js'; + +import { + createRepository, + deleteRepository, + getProjectRepositories, + syncRepository, + updateRepository, +} from './business.js'; export function repositoryRouter() { - return serverInstance.router(repositoryContract, { - // Récupérer tous les repositories d'un projet - listRepositories: async ({ request: req, query }) => { - const projectId = query.projectId - const perms = await authUser(req, { id: projectId }) - - const body = ProjectAuthorized.ListRepositories(perms) - ? await getProjectRepositories(projectId) - : [] - - return { - status: 200, - body, - } - }, - - // Synchroniser un repository - syncRepository: async ({ request: req, params, body }) => { - const { repositoryId } = params - const perms = await authUser(req, { repositoryId }) - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const { syncAllBranches, branchName } = body - - const resBody = await syncRepository({ repositoryId, userId: perms.user.id, branchName, requestId: req.id, syncAllBranches }) - if (resBody instanceof ErrorResType) return resBody - - return { - status: 204, - body: resBody, - } - }, - - // Créer un repository - createRepository: async ({ request: req, body: data }) => { - const projectId = data.projectId - const perms = await authUser(req, { id: projectId }) - - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const body = await createRepository({ data, userId: perms.user.id, requestId: req.id }) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - // Mettre à jour un repository - updateRepository: async ({ request: req, params, body }) => { - const repositoryId = params.repositoryId - const perms = await authUser(req, { repositoryId }) - - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const keysAllowedForUpdate = [ - 'externalRepoUrl', - 'isPrivate', - 'externalToken', - 'externalUserName', - 'isInfra', - ] - const data = filterObjectByKeys(body, keysAllowedForUpdate) - - if (data.externalToken === fakeToken) { - delete data.externalToken - } - - if (data.isPrivate === false) { - delete data.externalToken - delete data.externalUserName - } - - const resBody = await updateRepository({ repositoryId, data, userId: perms.user.id, requestId: req.id }) - if (resBody instanceof ErrorResType) return resBody - - return { - status: 200, - body: resBody, - } - }, - - // Supprimer un repository - deleteRepository: async ({ request: req, params }) => { - const repositoryId = params.repositoryId - const perms = await authUser(req, { repositoryId }) - - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const body = await deleteRepository({ - repositoryId, - userId: perms.user.id, - requestId: req.id, - projectId: perms.projectId, - }) - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - }) + return serverInstance.router(repositoryContract, { + // Récupérer tous les repositories d'un projet + listRepositories: async ({ request: req, query }) => { + const projectId = query.projectId; + const perms = await authUser(req, { id: projectId }); + + const body = ProjectAuthorized.ListRepositories(perms) + ? await getProjectRepositories(projectId) + : []; + + return { + status: 200, + body, + }; + }, + + // Synchroniser un repository + syncRepository: async ({ request: req, params, body }) => { + const { repositoryId } = params; + const perms = await authUser(req, { repositoryId }); + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const { syncAllBranches, branchName } = body; + + const resBody = await syncRepository({ + repositoryId, + userId: perms.user.id, + branchName, + requestId: req.id, + syncAllBranches, + }); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 204, + body: resBody, + }; + }, + + // Créer un repository + createRepository: async ({ request: req, body: data }) => { + const projectId = data.projectId; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await createRepository({ + data, + userId: perms.user.id, + requestId: req.id, + }); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Mettre à jour un repository + updateRepository: async ({ request: req, params, body }) => { + const repositoryId = params.repositoryId; + const perms = await authUser(req, { repositoryId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const keysAllowedForUpdate = [ + 'externalRepoUrl', + 'isPrivate', + 'externalToken', + 'externalUserName', + 'isInfra', + ]; + const data = filterObjectByKeys(body, keysAllowedForUpdate); + + if (data.externalToken === fakeToken) { + delete data.externalToken; + } + + if (data.isPrivate === false) { + delete data.externalToken; + delete data.externalUserName; + } + + const resBody = await updateRepository({ + repositoryId, + data, + userId: perms.user.id, + requestId: req.id, + }); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 200, + body: resBody, + }; + }, + + // Supprimer un repository + deleteRepository: async ({ request: req, params }) => { + const repositoryId = params.repositoryId; + const perms = await authUser(req, { repositoryId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await deleteRepository({ + repositoryId, + userId: perms.user.id, + requestId: req.id, + projectId: perms.projectId, + }); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts index 7e082d817..883e7e948 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts @@ -1,171 +1,174 @@ import type { - ServiceChain, - ServiceChainDetails, - ServiceChainFlows, -} from '@cpn-console/shared' + ServiceChain, + ServiceChainDetails, + ServiceChainFlows, +} from '@cpn-console/shared'; import { - serviceChainEnvironmentEnum, - serviceChainFlowStateEnum, - serviceChainLocationEnum, - serviceChainNetworkEnum, - serviceChainStateEnum, -} from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import axios from 'axios' -import type { Mock } from 'vitest' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + serviceChainEnvironmentEnum, + serviceChainFlowStateEnum, + serviceChainLocationEnum, + serviceChainNetworkEnum, + serviceChainStateEnum, +} from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import axios from 'axios'; +import type { Mock } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { - getServiceChainDetails, - getServiceChainFlows, - listServiceChains, - retryServiceChain, - validateServiceChain, -} from './business.ts' + getServiceChainDetails, + getServiceChainFlows, + listServiceChains, + retryServiceChain, + validateServiceChain, +} from './business.ts'; -vi.mock('axios') +vi.mock('axios'); -let serviceChain: ServiceChain -let serviceChainDetails: ServiceChainDetails -let serviceChainFlows: ServiceChainFlows +let serviceChain: ServiceChain; +let serviceChainDetails: ServiceChainDetails; +let serviceChainFlows: ServiceChainFlows; describe('test ServiceChain business logic', () => { - beforeEach(() => { - serviceChain = { - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement(serviceChainNetworkEnum), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - } - - serviceChainDetails = { - ...serviceChain, - validationId: faker.string.uuid(), - validatedBy: faker.helpers.maybe(() => faker.string.uuid()) || null, - ref: faker.string.uuid(), - location: faker.helpers.arrayElement(serviceChainLocationEnum), - targetAddress: faker.internet.ipv4(), - projectId: faker.string.uuid(), - env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), - subjectAlternativeName: faker.helpers.uniqueArray( - faker.internet.domainName, - 3, - ), - redirect: faker.datatype.boolean(), - antivirus: - faker.helpers.maybe(() => ({ - maxFileSize: faker.number.int(), - })) || null, // undefined is not wanted here - websocket: faker.datatype.boolean(), - ipWhiteList: faker.helpers - .uniqueArray(faker.internet.ipv4, 5) - .map(e => `${e}/32`), // We want a CIDR here - sslOutgoing: faker.datatype.boolean(), - } - - serviceChainFlows = { - reserve_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - create_cert: faker.helpers.maybe(() => ({ - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - })) || null, - call_exec: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - activate_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - dns_request: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - } - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('listServiceChains', () => { - it('should return a list of service chains', async () => { - const input = [serviceChain]; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }) - - const result = await listServiceChains() - - expect(result).toStrictEqual(input) - }) - }) - - describe('getServiceChainDetails', () => { - it('should return a service chain details', async () => { - const input = serviceChainDetails; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }) - - const result = await getServiceChainDetails(faker.string.uuid()) - - expect(result).toStrictEqual(input) - }) - }) - - describe('retryServiceChain', () => { - it('should trigger a service chain retry attempt', async () => { - const input = {}; - (axios.create as Mock).mockReturnValue({ - post: () => ({ data: input }), - }) - - const result = await retryServiceChain(faker.string.uuid()) - - expect(result.data).toStrictEqual(input) - }) - }) - - describe('validateServiceChain', () => { - it('should trigger a service chain validate attempt', async () => { - const input = {}; - (axios.create as Mock).mockReturnValue({ - post: () => ({ data: input }), - }) - - const result = await validateServiceChain(faker.string.uuid()) - - expect(result.data).toStrictEqual(input) - }) - }) - - describe('getServiceChainFlows', () => { - it('should return a service chain flows', async () => { - const input = serviceChainFlows; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }) - - const result = await getServiceChainFlows(faker.string.uuid()) - - expect(result).toStrictEqual(input) - }) - }) -}) + beforeEach(() => { + serviceChain = { + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement(serviceChainNetworkEnum), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + }; + + serviceChainDetails = { + ...serviceChain, + validationId: faker.string.uuid(), + validatedBy: faker.helpers.maybe(() => faker.string.uuid()) || null, + ref: faker.string.uuid(), + location: faker.helpers.arrayElement(serviceChainLocationEnum), + targetAddress: faker.internet.ipv4(), + projectId: faker.string.uuid(), + env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), + subjectAlternativeName: faker.helpers.uniqueArray( + faker.internet.domainName, + 3, + ), + redirect: faker.datatype.boolean(), + antivirus: + faker.helpers.maybe(() => ({ + maxFileSize: faker.number.int(), + })) || null, // undefined is not wanted here + websocket: faker.datatype.boolean(), + ipWhiteList: faker.helpers + .uniqueArray(faker.internet.ipv4, 5) + .map((e) => `${e}/32`), // We want a CIDR here + sslOutgoing: faker.datatype.boolean(), + }; + + serviceChainFlows = { + reserve_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + create_cert: + faker.helpers.maybe(() => ({ + state: faker.helpers.arrayElement( + serviceChainFlowStateEnum, + ), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + })) || null, + call_exec: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + activate_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + dns_request: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('listServiceChains', () => { + it('should return a list of service chains', async () => { + const input = [serviceChain]; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }); + + const result = await listServiceChains(); + + expect(result).toStrictEqual(input); + }); + }); + + describe('getServiceChainDetails', () => { + it('should return a service chain details', async () => { + const input = serviceChainDetails; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }); + + const result = await getServiceChainDetails(faker.string.uuid()); + + expect(result).toStrictEqual(input); + }); + }); + + describe('retryServiceChain', () => { + it('should trigger a service chain retry attempt', async () => { + const input = {}; + (axios.create as Mock).mockReturnValue({ + post: () => ({ data: input }), + }); + + const result = await retryServiceChain(faker.string.uuid()); + + expect(result.data).toStrictEqual(input); + }); + }); + + describe('validateServiceChain', () => { + it('should trigger a service chain validate attempt', async () => { + const input = {}; + (axios.create as Mock).mockReturnValue({ + post: () => ({ data: input }), + }); + + const result = await validateServiceChain(faker.string.uuid()); + + expect(result.data).toStrictEqual(input); + }); + }); + + describe('getServiceChainFlows', () => { + it('should return a service chain flows', async () => { + const input = serviceChainFlows; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }); + + const result = await getServiceChainFlows(faker.string.uuid()); + + expect(result).toStrictEqual(input); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts index baabb00a0..bfe1ba32e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts @@ -1,27 +1,27 @@ import { - getServiceChainDetails as getServiceChainDetailsQuery, - listServiceChains as listServiceChainsQuery, - retryServiceChain as retryServiceChainQuery, - validateServiceChain as validateServiceChainQuery, - getServiceChainFlows as getServiceChainFlowsQuery, -} from '@old-server/resources/queries-index.js' + getServiceChainDetails as getServiceChainDetailsQuery, + getServiceChainFlows as getServiceChainFlowsQuery, + listServiceChains as listServiceChainsQuery, + retryServiceChain as retryServiceChainQuery, + validateServiceChain as validateServiceChainQuery, +} from '@old-server/resources/queries-index.js'; export async function listServiceChains() { - return listServiceChainsQuery() + return listServiceChainsQuery(); } export async function getServiceChainDetails(serviceChainId: string) { - return getServiceChainDetailsQuery(serviceChainId) + return getServiceChainDetailsQuery(serviceChainId); } export async function retryServiceChain(serviceChainId: string) { - return retryServiceChainQuery(serviceChainId) + return retryServiceChainQuery(serviceChainId); } export async function validateServiceChain(validationId: string) { - return validateServiceChainQuery(validationId) + return validateServiceChainQuery(validationId); } export async function getServiceChainFlows(serviceChainId: string) { - return getServiceChainFlowsQuery(serviceChainId) + return getServiceChainFlowsQuery(serviceChainId); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts index 10713007c..6ffbe5f77 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts @@ -1,58 +1,58 @@ import { - type ServiceChain, - ServiceChainDetailsSchema, - ServiceChainFlowsSchema, - ServiceChainListSchema, -} from '@cpn-console/shared' -import axios from 'axios' -import https from 'node:https' + type ServiceChain, + ServiceChainDetailsSchema, + ServiceChainFlowsSchema, + ServiceChainListSchema, +} from '@cpn-console/shared'; +import axios from 'axios'; +import https from 'node:https'; -const openCDSEnvVar = 'OPENCDS_URL' -const openCDSTargetURL = process.env[openCDSEnvVar] -const openCDSDisabledErrorMessage = `OpenCDS is disabled, please set ${openCDSEnvVar} in your relevant .env file. See .env-example` +const openCDSEnvVar = 'OPENCDS_URL'; +const openCDSTargetURL = process.env[openCDSEnvVar]; +const openCDSDisabledErrorMessage = `OpenCDS is disabled, please set ${openCDSEnvVar} in your relevant .env file. See .env-example`; function getClient() { - if (!openCDSTargetURL) { - throw new Error(openCDSDisabledErrorMessage) - } - return axios.create({ - baseURL: openCDSTargetURL, - httpsAgent: new https.Agent({ - rejectUnauthorized: - // We want it to be `false` only if it has explicitly - // been stated as "false" in the env vars - process.env.OPENCDS_API_TLS_REJECT_UNAUTHORIZED !== 'false', - }), - headers: { - 'X-API-Key': process.env.OPENCDS_API_TOKEN, - }, - }) + if (!openCDSTargetURL) { + throw new Error(openCDSDisabledErrorMessage); + } + return axios.create({ + baseURL: openCDSTargetURL, + httpsAgent: new https.Agent({ + rejectUnauthorized: + // We want it to be `false` only if it has explicitly + // been stated as "false" in the env vars + process.env.OPENCDS_API_TLS_REJECT_UNAUTHORIZED !== 'false', + }), + headers: { + 'X-API-Key': process.env.OPENCDS_API_TOKEN, + }, + }); } export async function listServiceChains() { - return ServiceChainListSchema.parse( - (await getClient().get(`/requests`)).data, - ) + return ServiceChainListSchema.parse( + (await getClient().get(`/requests`)).data, + ); } export async function getServiceChainDetails( - serviceChainId: ServiceChain['id'], + serviceChainId: ServiceChain['id'], ) { - return ServiceChainDetailsSchema.parse( - (await getClient().get(`/requests/${serviceChainId}`)).data, - ) + return ServiceChainDetailsSchema.parse( + (await getClient().get(`/requests/${serviceChainId}`)).data, + ); } export async function retryServiceChain(serviceChainId: ServiceChain['id']) { - return await getClient().post(`/requests/${serviceChainId}/retry`) + return await getClient().post(`/requests/${serviceChainId}/retry`); } export async function validateServiceChain(validationId: string) { - return await getClient().post(`/validate/${validationId}`) + return await getClient().post(`/validate/${validationId}`); } export async function getServiceChainFlows(serviceChainId: ServiceChain['id']) { - return ServiceChainFlowsSchema.parse( - (await getClient().get(`/requests/${serviceChainId}/flows`)).data, - ) + return ServiceChainFlowsSchema.parse( + (await getClient().get(`/requests/${serviceChainId}/flows`)).data, + ); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts index 4edf4438f..1b7322956 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts @@ -1,306 +1,340 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ServiceChain, ServiceChainDetails, ServiceChainFlows } from '@cpn-console/shared' +import type { + ServiceChain, + ServiceChainDetails, + ServiceChainFlows, +} from '@cpn-console/shared'; import { - ServiceChainDetailsSchema, - ServiceChainFlowsSchema, - ServiceChainListSchema, - serviceChainContract, - serviceChainEnvironmentEnum, - serviceChainFlowStateEnum, - serviceChainLocationEnum, - serviceChainNetworkEnum, - serviceChainStateEnum, -} from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getUserMockInfos } from '../../utils/mocks.js' -import * as business from './business.js' + ServiceChainDetailsSchema, + ServiceChainFlowsSchema, + ServiceChainListSchema, + serviceChainContract, + serviceChainEnvironmentEnum, + serviceChainFlowStateEnum, + serviceChainLocationEnum, + serviceChainNetworkEnum, + serviceChainStateEnum, +} from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, -) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListServiceChainsMock = vi.spyOn(business, 'listServiceChains') -const businessGetServiceChainDetailsMock = vi.spyOn(business, 'getServiceChainDetails') -const businessRetryServiceChainMock = vi.spyOn(business, 'retryServiceChain') -const businessValidateServiceChainMock = vi.spyOn(business, 'validateServiceChain') -const businessGetServiceChainsFlowsMock = vi.spyOn(business, 'getServiceChainFlows') + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessListServiceChainsMock = vi.spyOn(business, 'listServiceChains'); +const businessGetServiceChainDetailsMock = vi.spyOn( + business, + 'getServiceChainDetails', +); +const businessRetryServiceChainMock = vi.spyOn(business, 'retryServiceChain'); +const businessValidateServiceChainMock = vi.spyOn( + business, + 'validateServiceChain', +); +const businessGetServiceChainsFlowsMock = vi.spyOn( + business, + 'getServiceChainFlows', +); describe('test ServiceChainContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - describe('listServiceChains', () => { - it('as non admin', async () => { - const user = getUserMockInfos(false) + beforeEach(() => { + vi.resetAllMocks(); + }); + describe('listServiceChains', () => { + it('as non admin', async () => { + const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user) + authUserMock.mockResolvedValueOnce(user); - businessListServiceChainsMock.mockResolvedValueOnce([]) - const response = await app - .inject() - .get(serviceChainContract.listServiceChains.path) - .end() + businessListServiceChainsMock.mockResolvedValueOnce([]); + const response = await app + .inject() + .get(serviceChainContract.listServiceChains.path) + .end(); - expect(response.json()).toStrictEqual([]) - expect(response.statusCode).toEqual(200) - }) - it('as admin', async () => { - const user = getUserMockInfos(true) - const serviceChainList = faker.helpers.multiple(() => ({ - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement(serviceChainNetworkEnum), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - })) + expect(response.json()).toStrictEqual([]); + expect(response.statusCode).toEqual(200); + }); + it('as admin', async () => { + const user = getUserMockInfos(true); + const serviceChainList = faker.helpers.multiple( + () => ({ + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement( + serviceChainNetworkEnum, + ), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + }), + ); - authUserMock.mockResolvedValueOnce(user) + authUserMock.mockResolvedValueOnce(user); - businessListServiceChainsMock.mockResolvedValueOnce(serviceChainList) - const response = await app - .inject() - .get(serviceChainContract.listServiceChains.path) - .end() + businessListServiceChainsMock.mockResolvedValueOnce( + serviceChainList, + ); + const response = await app + .inject() + .get(serviceChainContract.listServiceChains.path) + .end(); - expect(businessListServiceChainsMock).toHaveBeenCalledWith() + expect(businessListServiceChainsMock).toHaveBeenCalledWith(); - expect(ServiceChainListSchema.parse(response.json())).toStrictEqual( - serviceChainList, - ) - expect(response.statusCode).toEqual(200) - }) - }) + expect(ServiceChainListSchema.parse(response.json())).toStrictEqual( + serviceChainList, + ); + expect(response.statusCode).toEqual(200); + }); + }); - describe('getServiceChainDetails', () => { - it('should return serviceChain details', async () => { - const serviceChainDetails: ServiceChainDetails = { - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement(serviceChainNetworkEnum), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - validationId: faker.string.uuid(), - validatedBy: faker.string.uuid(), - ref: faker.string.uuid(), - location: faker.helpers.arrayElement(serviceChainLocationEnum), - targetAddress: faker.internet.ipv4(), - projectId: faker.string.uuid(), - env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), - subjectAlternativeName: faker.helpers.uniqueArray( - faker.internet.domainName, - 3, - ), - redirect: faker.datatype.boolean(), - antivirus: - faker.helpers.maybe(() => ({ - maxFileSize: faker.number.int(), - })) || null, // undefined is not wanted here - websocket: faker.datatype.boolean(), - ipWhiteList: faker.helpers - .uniqueArray(faker.internet.ipv4, 5) - .map(e => `${e}/32`), // We want a CIDR here - sslOutgoing: faker.datatype.boolean(), - } - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) + describe('getServiceChainDetails', () => { + it('should return serviceChain details', async () => { + const serviceChainDetails: ServiceChainDetails = { + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement(serviceChainNetworkEnum), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + validationId: faker.string.uuid(), + validatedBy: faker.string.uuid(), + ref: faker.string.uuid(), + location: faker.helpers.arrayElement(serviceChainLocationEnum), + targetAddress: faker.internet.ipv4(), + projectId: faker.string.uuid(), + env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), + subjectAlternativeName: faker.helpers.uniqueArray( + faker.internet.domainName, + 3, + ), + redirect: faker.datatype.boolean(), + antivirus: + faker.helpers.maybe(() => ({ + maxFileSize: faker.number.int(), + })) || null, // undefined is not wanted here + websocket: faker.datatype.boolean(), + ipWhiteList: faker.helpers + .uniqueArray(faker.internet.ipv4, 5) + .map((e) => `${e}/32`), // We want a CIDR here + sslOutgoing: faker.datatype.boolean(), + }; + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); - businessGetServiceChainDetailsMock.mockResolvedValueOnce(serviceChainDetails) - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainDetails.path.replace( - ':serviceChainId', - serviceChainDetails.id, - ), - ) - .end() + businessGetServiceChainDetailsMock.mockResolvedValueOnce( + serviceChainDetails, + ); + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainDetails.path.replace( + ':serviceChainId', + serviceChainDetails.id, + ), + ) + .end(); - expect(ServiceChainDetailsSchema.parse(response.json())).toEqual( - serviceChainDetails, - ) - expect(response.statusCode).toEqual(200) - expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(1) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) + expect(ServiceChainDetailsSchema.parse(response.json())).toEqual( + serviceChainDetails, + ); + expect(response.statusCode).toEqual(200); + expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(1); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainDetails.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainDetails.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end(); - expect(response.statusCode).toEqual(403) - expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(0) - }) - }) + expect(response.statusCode).toEqual(403); + expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(0); + }); + }); - describe('retryServiceChain', () => { - it('should return 204', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) + describe('retryServiceChain', () => { + it('should return 204', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); - businessRetryServiceChainMock.mockResolvedValueOnce({ - status: 204, - body: undefined, - }) - const response = await app - .inject() - .post( - serviceChainContract.retryServiceChain.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() + businessRetryServiceChainMock.mockResolvedValueOnce({ + status: 204, + body: undefined, + }); + const response = await app + .inject() + .post( + serviceChainContract.retryServiceChain.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end(); - expect(response.body).toEqual('') - expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(1) - expect(response.statusCode).toEqual(204) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) + expect(response.body).toEqual(''); + expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(1); + expect(response.statusCode).toEqual(204); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); - const response = await app - .inject() - .post( - serviceChainContract.retryServiceChain.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() + const response = await app + .inject() + .post( + serviceChainContract.retryServiceChain.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end(); - expect(response.statusCode).toEqual(403) - expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(0) - }) - }) + expect(response.statusCode).toEqual(403); + expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(0); + }); + }); - describe('validateServiceChain', () => { - it('should return 204', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) + describe('validateServiceChain', () => { + it('should return 204', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); - businessValidateServiceChainMock.mockResolvedValueOnce({ - status: 204, - body: undefined, - }) - const response = await app - .inject() - .post( - serviceChainContract.validateServiceChain.path.replace( - ':validationId', - faker.string.uuid(), - ), - ) - .end() + businessValidateServiceChainMock.mockResolvedValueOnce({ + status: 204, + body: undefined, + }); + const response = await app + .inject() + .post( + serviceChainContract.validateServiceChain.path.replace( + ':validationId', + faker.string.uuid(), + ), + ) + .end(); - expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(1) - expect(response.body).toEqual('') - expect(response.statusCode).toEqual(204) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) + expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(1); + expect(response.body).toEqual(''); + expect(response.statusCode).toEqual(204); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); - const response = await app - .inject() - .post( - serviceChainContract.validateServiceChain.path.replace( - ':validationId', - faker.string.uuid(), - ), - ) - .end() + const response = await app + .inject() + .post( + serviceChainContract.validateServiceChain.path.replace( + ':validationId', + faker.string.uuid(), + ), + ) + .end(); - expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) + expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); - describe('getServiceChainFlows', () => { - it('should return serviceChain flows', async () => { - const serviceChainFlows: ServiceChainFlows = { - reserve_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - create_cert: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - call_exec: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - activate_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - dns_request: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - } - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) + describe('getServiceChainFlows', () => { + it('should return serviceChain flows', async () => { + const serviceChainFlows: ServiceChainFlows = { + reserve_ip: { + state: faker.helpers.arrayElement( + serviceChainFlowStateEnum, + ), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + create_cert: { + state: faker.helpers.arrayElement( + serviceChainFlowStateEnum, + ), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + call_exec: { + state: faker.helpers.arrayElement( + serviceChainFlowStateEnum, + ), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + activate_ip: { + state: faker.helpers.arrayElement( + serviceChainFlowStateEnum, + ), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + dns_request: { + state: faker.helpers.arrayElement( + serviceChainFlowStateEnum, + ), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + }; + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); - businessGetServiceChainsFlowsMock.mockResolvedValueOnce(serviceChainFlows) - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainFlows.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() + businessGetServiceChainsFlowsMock.mockResolvedValueOnce( + serviceChainFlows, + ); + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainFlows.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end(); - expect(ServiceChainFlowsSchema.parse(response.json())).toEqual( - serviceChainFlows, - ) - expect(response.statusCode).toEqual(200) - expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(1) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) + expect(ServiceChainFlowsSchema.parse(response.json())).toEqual( + serviceChainFlows, + ); + expect(response.statusCode).toEqual(200); + expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(1); + }); + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainFlows.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainFlows.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end(); - expect(response.statusCode).toEqual(403) - expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(0) - }) - }) -}) + expect(response.statusCode).toEqual(403); + expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts index adada395a..aec3ea1e4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts @@ -1,90 +1,90 @@ -import type { AsyncReturnType } from '@cpn-console/shared' -import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared' +import type { AsyncReturnType } from '@cpn-console/shared'; +import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import '@old-server/types/index.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { Forbidden403 } from '@old-server/utils/errors.js'; + import { - listServiceChains as listServiceChainsBusiness, - getServiceChainDetails as getServiceChainDetailsBusiness, - retryServiceChain as retryServiceChainBusiness, - validateServiceChain as validateServiceChainBusiness, - getServiceChainFlows as getServiceChainFlowsBusiness, -} from './business.js' -import '@old-server/types/index.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { Forbidden403 } from '@old-server/utils/errors.js' + getServiceChainDetails as getServiceChainDetailsBusiness, + getServiceChainFlows as getServiceChainFlowsBusiness, + listServiceChains as listServiceChainsBusiness, + retryServiceChain as retryServiceChainBusiness, + validateServiceChain as validateServiceChainBusiness, +} from './business.js'; export function serviceChainRouter() { - return serverInstance.router(serviceChainContract, { - listServiceChains: async ({ request: req }) => { - const { adminPermissions } = await authUser(req) - - let body: AsyncReturnType = [] - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listServiceChainsBusiness() - } - - return { - status: 200, - body, - } - }, - - getServiceChainDetails: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403() - - const serviceChainId = params.serviceChainId - const serviceChainDetails - = await getServiceChainDetailsBusiness(serviceChainId) - - return { - status: 200, - body: serviceChainDetails, - } - }, - - retryServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403() - - const serviceChainId = params.serviceChainId - await retryServiceChainBusiness(serviceChainId) - - return { - status: 204, - body: null, - } - }, - - validateServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403() - - const serviceChainId = params.validationId - await validateServiceChainBusiness(serviceChainId) - - return { - status: 204, - body: null, - } - }, - - getServiceChainFlows: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403() - - const serviceChainId = params.serviceChainId - const serviceChainFlows - = await getServiceChainFlowsBusiness(serviceChainId) - - return { - status: 200, - body: serviceChainFlows, - } - }, - - }) + return serverInstance.router(serviceChainContract, { + listServiceChains: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + + let body: AsyncReturnType = []; + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listServiceChainsBusiness(); + } + + return { + status: 200, + body, + }; + }, + + getServiceChainDetails: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + const serviceChainDetails = + await getServiceChainDetailsBusiness(serviceChainId); + + return { + status: 200, + body: serviceChainDetails, + }; + }, + + retryServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + await retryServiceChainBusiness(serviceChainId); + + return { + status: 204, + body: null, + }; + }, + + validateServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.validationId; + await validateServiceChainBusiness(serviceChainId); + + return { + status: 204, + body: null, + }; + }, + + getServiceChainFlows: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + const serviceChainFlows = + await getServiceChainFlowsBusiness(serviceChainId); + + return { + status: 200, + body: serviceChainFlows, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts index fa61d5a6d..829e2e172 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts @@ -1,9 +1,9 @@ -import { services } from '@cpn-console/hooks' +import { services } from '@cpn-console/hooks'; export function checkServicesHealth() { - return services.getStatus() + return services.getStatus(); } export async function refreshServicesHealth() { - return Promise.all(services.refreshStatus()) + return Promise.all(services.refreshStatus()); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts index 1ec2528fc..e8b6cc45d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts @@ -1,78 +1,104 @@ -import { describe, expect, it, vi } from 'vitest' -import { MonitorStatus, serviceContract } from '@cpn-console/shared' -import type { ServiceStatus } from '@cpn-console/hooks' -import app from '../../app.js' -import * as business from './business.js' -import { getUserMockInfos } from '../../utils/mocks.js' -import * as utilsController from '../../utils/controller.js' +import type { ServiceStatus } from '@cpn-console/hooks'; +import { MonitorStatus, serviceContract } from '@cpn-console/shared'; +import { describe, expect, it, vi } from 'vitest'; -const authUserMock = vi.spyOn(utilsController, 'authUser') +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const businessCheckMock = vi.spyOn(business, 'checkServicesHealth') -const businessRefreshMock = vi.spyOn(business, 'refreshServicesHealth') +const authUserMock = vi.spyOn(utilsController, 'authUser'); + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const businessCheckMock = vi.spyOn(business, 'checkServicesHealth'); +const businessRefreshMock = vi.spyOn(business, 'refreshServicesHealth'); describe('test serviceContract', () => { - const services: ServiceStatus[] = [{ interval: 1, lastUpdateTimestamp: 1, message: 'OK', name: 'A service', status: MonitorStatus.OK }] - const servicesComplete: ServiceStatus[] = [{ cause: 'error', interval: 1, lastUpdateTimestamp: 1, message: 'OK', name: 'A service', status: MonitorStatus.OK }] - - it('should return complete services, with cause', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessCheckMock.mockReturnValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end() - - expect(response.json()).toStrictEqual(servicesComplete) - expect(response.statusCode).toEqual(200) - }) - - it('should not return complete services, forbidden', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - businessCheckMock.mockReturnValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return services', async () => { - businessCheckMock.mockReturnValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getServiceHealth.path) - .end() - - expect(response.json()).toStrictEqual(services) - expect(response.statusCode).toEqual(200) - }) - - it('should refresh services', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessRefreshMock.mockResolvedValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end() - - expect(response.json()).toStrictEqual(servicesComplete) - expect(response.statusCode).toEqual(200) - }) - - it('should refresh services, cause forbidden', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - businessRefreshMock.mockResolvedValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end() - - expect(response.statusCode).toEqual(403) - }) -}) + const services: ServiceStatus[] = [ + { + interval: 1, + lastUpdateTimestamp: 1, + message: 'OK', + name: 'A service', + status: MonitorStatus.OK, + }, + ]; + const servicesComplete: ServiceStatus[] = [ + { + cause: 'error', + interval: 1, + lastUpdateTimestamp: 1, + message: 'OK', + name: 'A service', + status: MonitorStatus.OK, + }, + ]; + + it('should return complete services, with cause', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessCheckMock.mockReturnValue(servicesComplete); + const response = await app + .inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end(); + + expect(response.json()).toStrictEqual(servicesComplete); + expect(response.statusCode).toEqual(200); + }); + + it('should not return complete services, forbidden', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + businessCheckMock.mockReturnValue(servicesComplete); + const response = await app + .inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end(); + + expect(response.statusCode).toEqual(403); + }); + + it('should return services', async () => { + businessCheckMock.mockReturnValue(servicesComplete); + const response = await app + .inject() + .get(serviceContract.getServiceHealth.path) + .end(); + + expect(response.json()).toStrictEqual(services); + expect(response.statusCode).toEqual(200); + }); + + it('should refresh services', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessRefreshMock.mockResolvedValue(servicesComplete); + const response = await app + .inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end(); + + expect(response.json()).toStrictEqual(servicesComplete); + expect(response.statusCode).toEqual(200); + }); + + it('should refresh services, cause forbidden', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + businessRefreshMock.mockResolvedValue(servicesComplete); + const response = await app + .inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end(); + + expect(response.statusCode).toEqual(403); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts index 6e5282767..02347ad14 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts @@ -1,43 +1,46 @@ -import { AdminAuthorized, serviceContract } from '@cpn-console/shared' -import { checkServicesHealth, refreshServicesHealth } from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { Forbidden403 } from '@old-server/utils/errors.js' +import { AdminAuthorized, serviceContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { Forbidden403 } from '@old-server/utils/errors.js'; + +import { checkServicesHealth, refreshServicesHealth } from './business.js'; export function serviceMonitorRouter() { - return serverInstance.router(serviceContract, { - getServiceHealth: async () => { - const serviceData = checkServicesHealth() - - return { - status: 200, - body: serviceData, - } - }, - - getCompleteServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req) - - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - const serviceData = checkServicesHealth() - - return { - status: 200, - body: serviceData, - } - }, - - refreshServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - - await refreshServicesHealth() - const serviceData = checkServicesHealth() - - return { - status: 200, - body: serviceData, - } - }, - }) + return serverInstance.router(serviceContract, { + getServiceHealth: async () => { + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + + getCompleteServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + + refreshServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + + await refreshServicesHealth(); + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts index d608ee0c0..e6dc2783f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts @@ -1,113 +1,169 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Environment, Stage } from '@prisma/client' -import prisma from '../../__mocks__/prisma.js' -import { BadRequest400, NotFound404 } from '../../utils/errors.ts' -import { createStage, deleteStage, getStageAssociatedEnvironments, listStages, updateStage } from './business.ts' +import { faker } from '@faker-js/faker'; +import type { Environment, Stage } from '@prisma/client'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import { BadRequest400, NotFound404 } from '../../utils/errors.ts'; +import { + createStage, + deleteStage, + getStageAssociatedEnvironments, + listStages, + updateStage, +} from './business.ts'; describe('test stage busines logic', () => { - let stage: Stage - beforeEach(() => { - vi.resetAllMocks() - stage = { - id: faker.string.uuid(), - name: faker.company.name(), - } - }) - describe('createStage', () => { - it('should create a stage', async () => { - prisma.stage.findUnique.mockResolvedValue(null) - prisma.stage.create.mockResolvedValue({ id: stage.id } as Stage) - await createStage({ name: stage.name, clusterIds: [faker.string.uuid()] }) - expect(prisma.stage.update).toHaveBeenCalledTimes(1) - }) - it('should not create a stage, name conflict', async () => { - prisma.stage.findUnique.mockResolvedValue({ id: stage.id } as Stage) - const response = await createStage({ name: stage.name, clusterIds: [faker.string.uuid()] }) - expect(prisma.stage.update).toHaveBeenCalledTimes(0) - expect(response).instanceOf(BadRequest400) - }) - }) + let stage: Stage; + beforeEach(() => { + vi.resetAllMocks(); + stage = { + id: faker.string.uuid(), + name: faker.company.name(), + }; + }); + describe('createStage', () => { + it('should create a stage', async () => { + prisma.stage.findUnique.mockResolvedValue(null); + prisma.stage.create.mockResolvedValue({ id: stage.id } as Stage); + await createStage({ + name: stage.name, + clusterIds: [faker.string.uuid()], + }); + expect(prisma.stage.update).toHaveBeenCalledTimes(1); + }); + it('should not create a stage, name conflict', async () => { + prisma.stage.findUnique.mockResolvedValue({ + id: stage.id, + } as Stage); + const response = await createStage({ + name: stage.name, + clusterIds: [faker.string.uuid()], + }); + expect(prisma.stage.update).toHaveBeenCalledTimes(0); + expect(response).instanceOf(BadRequest400); + }); + }); - describe('updateStage', () => { - it('should update a stage', async () => { - const dbClusters = [{ id: faker.string.uuid() }] - const newClusters = [faker.string.uuid()] - prisma.stage.findUnique.mockResolvedValue({ ...stage, clusters: dbClusters } as Stage) - prisma.stage.update.mockResolvedValue({ id: stage.id } as Stage) - const response = await updateStage(stage.id, { name: stage.name, clusterIds: newClusters }) - expect(prisma.cluster.update).toHaveBeenCalledTimes(1) - expect(prisma.cluster.update).toHaveBeenCalledWith({ where: { id: dbClusters[0].id }, data: { - stages: { - disconnect: { - id: stage.id, - }, - }, - } }) - expect(prisma.stage.update).toHaveBeenCalledTimes(1) - expect(prisma.stage.update).toHaveBeenCalledWith({ where: { id: stage.id }, data: { - clusters: { - connect: [{ - id: newClusters[0], - }], - }, - } }) - expect(response.clusterIds).toBe(newClusters) - }) - it('should do nothing', async () => { - prisma.stage.findUnique.mockResolvedValue({ ...stage, clusters: [] } as Stage) - await updateStage(stage.id, { clusterIds: [], name: stage.name }) - expect(prisma.stage.update).toHaveBeenCalledTimes(0) - }) - it('should return not found', async () => { - prisma.stage.findUnique.mockResolvedValue(null) - const response = await updateStage(stage.id, { name: stage.name, clusterIds: [faker.string.uuid()] }) - expect(prisma.stage.update).toHaveBeenCalledTimes(0) - expect(response).instanceOf(NotFound404) - }) - }) + describe('updateStage', () => { + it('should update a stage', async () => { + const dbClusters = [{ id: faker.string.uuid() }]; + const newClusters = [faker.string.uuid()]; + prisma.stage.findUnique.mockResolvedValue({ + ...stage, + clusters: dbClusters, + } as Stage); + prisma.stage.update.mockResolvedValue({ id: stage.id } as Stage); + const response = await updateStage(stage.id, { + name: stage.name, + clusterIds: newClusters, + }); + expect(prisma.cluster.update).toHaveBeenCalledTimes(1); + expect(prisma.cluster.update).toHaveBeenCalledWith({ + where: { id: dbClusters[0].id }, + data: { + stages: { + disconnect: { + id: stage.id, + }, + }, + }, + }); + expect(prisma.stage.update).toHaveBeenCalledTimes(1); + expect(prisma.stage.update).toHaveBeenCalledWith({ + where: { id: stage.id }, + data: { + clusters: { + connect: [ + { + id: newClusters[0], + }, + ], + }, + }, + }); + expect(response.clusterIds).toBe(newClusters); + }); + it('should do nothing', async () => { + prisma.stage.findUnique.mockResolvedValue({ + ...stage, + clusters: [], + } as Stage); + await updateStage(stage.id, { clusterIds: [], name: stage.name }); + expect(prisma.stage.update).toHaveBeenCalledTimes(0); + }); + it('should return not found', async () => { + prisma.stage.findUnique.mockResolvedValue(null); + const response = await updateStage(stage.id, { + name: stage.name, + clusterIds: [faker.string.uuid()], + }); + expect(prisma.stage.update).toHaveBeenCalledTimes(0); + expect(response).instanceOf(NotFound404); + }); + }); - describe('deleteStage', () => { - it('should delete a stage', async () => { - prisma.environment.findFirst.mockResolvedValue(null) - prisma.stage.delete.mockResolvedValue({ id: stage.id } as Stage) - await deleteStage(stage.id) - expect(prisma.stage.delete).toHaveBeenCalledTimes(1) - }) - it('should not delete a stage, environment attached', async () => { - prisma.environment.findFirst.mockResolvedValue({ id: faker.string.uuid() } as Environment) - const response = await deleteStage(stage.id) - expect(prisma.stage.delete).toHaveBeenCalledTimes(0) - expect(response).instanceOf(BadRequest400) - }) - }) + describe('deleteStage', () => { + it('should delete a stage', async () => { + prisma.environment.findFirst.mockResolvedValue(null); + prisma.stage.delete.mockResolvedValue({ id: stage.id } as Stage); + await deleteStage(stage.id); + expect(prisma.stage.delete).toHaveBeenCalledTimes(1); + }); + it('should not delete a stage, environment attached', async () => { + prisma.environment.findFirst.mockResolvedValue({ + id: faker.string.uuid(), + } as Environment); + const response = await deleteStage(stage.id); + expect(prisma.stage.delete).toHaveBeenCalledTimes(0); + expect(response).instanceOf(BadRequest400); + }); + }); - describe('listStages', () => { - const clusterAssociated = [{ id: faker.string.uuid() }] - it('should list all stages (admin, no userId provided)', async () => { - prisma.stage.findMany.mockResolvedValue([{ clusters: clusterAssociated }] as unknown as Stage[]) - const response = await listStages() - expect(response[0].clusterIds).toStrictEqual([clusterAssociated[0].id]) - expect(prisma.stage.findMany).toHaveBeenCalledTimes(1) - expect(prisma.stage.findMany).toHaveBeenCalledWith({ include: { clusters: true } }) - }) - }) + describe('listStages', () => { + const clusterAssociated = [{ id: faker.string.uuid() }]; + it('should list all stages (admin, no userId provided)', async () => { + prisma.stage.findMany.mockResolvedValue([ + { clusters: clusterAssociated }, + ] as unknown as Stage[]); + const response = await listStages(); + expect(response[0].clusterIds).toStrictEqual([ + clusterAssociated[0].id, + ]); + expect(prisma.stage.findMany).toHaveBeenCalledTimes(1); + expect(prisma.stage.findMany).toHaveBeenCalledWith({ + include: { clusters: true }, + }); + }); + }); - describe('getStageAssociatedEnvironments', () => { - it('should list all environments attached to a stage stages', async () => { - const envName = faker.string.alpha(8) - const projectSlug = faker.string.alpha(8) - const clusterLabel = faker.string.alpha(8) - const ownerEmail = faker.internet.email() - const envs = [{ name: envName, project: { slug: projectSlug, owner: { email: ownerEmail } }, cluster: { label: clusterLabel } }] - prisma.environment.findMany.mockResolvedValue(envs as unknown as Environment[]) - const response = await getStageAssociatedEnvironments(stage.id) - expect(response).toStrictEqual([{ - name: envName, - project: projectSlug, - owner: ownerEmail, - cluster: clusterLabel, - }]) - }) - }) -}) + describe('getStageAssociatedEnvironments', () => { + it('should list all environments attached to a stage stages', async () => { + const envName = faker.string.alpha(8); + const projectSlug = faker.string.alpha(8); + const clusterLabel = faker.string.alpha(8); + const ownerEmail = faker.internet.email(); + const envs = [ + { + name: envName, + project: { + slug: projectSlug, + owner: { email: ownerEmail }, + }, + cluster: { label: clusterLabel }, + }, + ]; + prisma.environment.findMany.mockResolvedValue( + envs as unknown as Environment[], + ); + const response = await getStageAssociatedEnvironments(stage.id); + expect(response).toStrictEqual([ + { + name: envName, + project: projectSlug, + owner: ownerEmail, + cluster: clusterLabel, + }, + ]); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts index d80296d3b..6950805f3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts @@ -1,97 +1,115 @@ -import type { Cluster, Stage } from '@prisma/client' -import type { CreateStageBody, UpdateStageBody } from '@cpn-console/shared' +import type { CreateStageBody, UpdateStageBody } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; import { - createStage as createStageQuery, - deleteStage as deleteStageQuery, - getAllStageIds, - getStageAssociatedEnvironmentById, - getStageById, - getStageByName, - linkClusterToStages as linkClusterToStagesQuery, - linkStageToClusters, - listStages as listStagesQuery, - removeClusterFromStage, - updateStageName, -} from '@old-server/resources/queries-index.js' -import { BadRequest400, NotFound404 } from '@old-server/utils/errors.js' -import prisma from '@old-server/prisma.js' + createStage as createStageQuery, + deleteStage as deleteStageQuery, + getAllStageIds, + getStageAssociatedEnvironmentById, + getStageById, + getStageByName, + linkClusterToStages as linkClusterToStagesQuery, + linkStageToClusters, + listStages as listStagesQuery, + removeClusterFromStage, + updateStageName, +} from '@old-server/resources/queries-index.js'; +import { BadRequest400, NotFound404 } from '@old-server/utils/errors.js'; +import type { Cluster, Stage } from '@prisma/client'; export async function getStageAssociatedEnvironments(stageId: Stage['id']) { - const environments = await getStageAssociatedEnvironmentById(stageId) - return environments.map(env => ({ - project: env.project.slug, - name: env.name, - cluster: env.cluster.label, - owner: env.project.owner.email, - })) + const environments = await getStageAssociatedEnvironmentById(stageId); + return environments.map((env) => ({ + project: env.project.slug, + name: env.name, + cluster: env.cluster.label, + owner: env.project.owner.email, + })); } export async function createStage({ clusterIds = [], name }: CreateStageBody) { - const isNameTaken = await getStageByName(name) - if (isNameTaken) return new BadRequest400('Un type d\'environnement portant ce nom existe déjà') + const isNameTaken = await getStageByName(name); + if (isNameTaken) + return new BadRequest400( + "Un type d'environnement portant ce nom existe déjà", + ); - const stage = await createStageQuery({ name }) + const stage = await createStageQuery({ name }); - if (clusterIds.length) { - await linkStageToClusters(stage.id, clusterIds) - } + if (clusterIds.length) { + await linkStageToClusters(stage.id, clusterIds); + } - return { - id: stage.id, - name: stage.name, - clusterIds, - } + return { + id: stage.id, + name: stage.name, + clusterIds, + }; } -export async function updateStage(stageId: Stage['id'], { clusterIds, name }: UpdateStageBody) { - const dbStage = await getStageById(stageId) - if (!dbStage) return new NotFound404() - if (name !== dbStage.name) { - await updateStageName(stageId, name) - } - // Remove clusters - const dbClusters = dbStage.clusters - if (dbClusters?.length) { - const clustersToRemove = dbClusters.filter(dbCluster => !clusterIds.includes(dbCluster.id)) - for (const clusterToRemove of clustersToRemove) { - await removeClusterFromStage(clusterToRemove.id, stageId) +export async function updateStage( + stageId: Stage['id'], + { clusterIds, name }: UpdateStageBody, +) { + const dbStage = await getStageById(stageId); + if (!dbStage) return new NotFound404(); + if (name !== dbStage.name) { + await updateStageName(stageId, name); + } + // Remove clusters + const dbClusters = dbStage.clusters; + if (dbClusters?.length) { + const clustersToRemove = dbClusters.filter( + (dbCluster) => !clusterIds.includes(dbCluster.id), + ); + for (const clusterToRemove of clustersToRemove) { + await removeClusterFromStage(clusterToRemove.id, stageId); + } + } + // Add clusters + if (clusterIds.length) { + await linkStageToClusters(stageId, clusterIds); } - } - // Add clusters - if (clusterIds.length) { - await linkStageToClusters(stageId, clusterIds) - } - return { - id: stageId, - name: name ?? dbStage.name, - clusterIds: clusterIds ?? dbStage.clusters.map(({ id }) => id), - } + return { + id: stageId, + name: name ?? dbStage.name, + clusterIds: clusterIds ?? dbStage.clusters.map(({ id }) => id), + }; } export async function deleteStage(stageId: Stage['id']) { - const attachedEnvironment = await prisma.environment.findFirst({ where: { stageId }, select: { id: true } }) - if (attachedEnvironment) return new BadRequest400('Impossible de supprimer le stage, des environnements en activité y ont souscrit') + const attachedEnvironment = await prisma.environment.findFirst({ + where: { stageId }, + select: { id: true }, + }); + if (attachedEnvironment) + return new BadRequest400( + 'Impossible de supprimer le stage, des environnements en activité y ont souscrit', + ); - await deleteStageQuery(stageId) - return null + await deleteStageQuery(stageId); + return null; } export async function listStages() { - const stages = await listStagesQuery() + const stages = await listStagesQuery(); - return stages.map((stage) => { - return { - id: stage.id, - name: stage.name, - clusterIds: stage.clusters.map(({ id }) => id), - } - }) + return stages.map((stage) => { + return { + id: stage.id, + name: stage.name, + clusterIds: stage.clusters.map(({ id }) => id), + }; + }); } -export async function linkClusterToStages(clusterId: Cluster['id'], stageIds: Stage['id'][], linkToAll: boolean = false) { - if (linkToAll === true) { - stageIds = await getAllStageIds() - } - await linkClusterToStagesQuery(clusterId, stageIds) +export async function linkClusterToStages( + clusterId: Cluster['id'], + stageIds: Stage['id'][], + linkToAll: boolean = false, +) { + if (linkToAll === true) { + stageIds = await getAllStageIds(); + } + await linkClusterToStagesQuery(clusterId, stageIds); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts index d5d4ea848..874f84789 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts @@ -1,111 +1,116 @@ -import type { Cluster, Stage } from '@prisma/client' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { Cluster, Stage } from '@prisma/client'; export function listStages() { - return prisma.stage.findMany({ - include: { - clusters: true, - }, - }) + return prisma.stage.findMany({ + include: { + clusters: true, + }, + }); } export async function getAllStageIds() { - return (await prisma.stage.findMany({ - select: { - id: true, - }, - })).map(({ id }) => id) + return ( + await prisma.stage.findMany({ + select: { + id: true, + }, + }) + ).map(({ id }) => id); } export function getStageById(id: Stage['id']) { - return prisma.stage.findUnique({ - where: { id }, - include: { - clusters: true, - }, - }) + return prisma.stage.findUnique({ + where: { id }, + include: { + clusters: true, + }, + }); } export function getStageByIdOrThrow(id: Stage['id']) { - return prisma.stage.findUniqueOrThrow({ - where: { id }, - include: { - clusters: true, - }, - }) + return prisma.stage.findUniqueOrThrow({ + where: { id }, + include: { + clusters: true, + }, + }); } export function getStageAssociatedEnvironmentById(id: Stage['id']) { - return prisma.environment.findMany({ - where: { - stageId: id, - }, - select: { - name: true, - cluster: { - select: { - label: true, + return prisma.environment.findMany({ + where: { + stageId: id, }, - }, - project: { select: { - name: true, - owner: true, - slug: true, + name: true, + cluster: { + select: { + label: true, + }, + }, + project: { + select: { + name: true, + owner: true, + slug: true, + }, + }, }, - }, - }, - }) + }); } export function getStageAssociatedEnvironmentLengthById(id: Stage['id']) { - return prisma.environment.count({ - where: { - stageId: id, - }, - }) + return prisma.environment.count({ + where: { + stageId: id, + }, + }); } export function getStageByName(name: Stage['name']) { - return prisma.stage.findUnique({ - where: { name }, - }) + return prisma.stage.findUnique({ + where: { name }, + }); } -export function linkStageToClusters(id: Stage['id'], clusterIds: Cluster['id'][]) { - return prisma.stage.update({ - where: { - id, - }, - data: { - clusters: { - connect: clusterIds.map(clusterId => ({ id: clusterId })), - }, - }, - }) +export function linkStageToClusters( + id: Stage['id'], + clusterIds: Cluster['id'][], +) { + return prisma.stage.update({ + where: { + id, + }, + data: { + clusters: { + connect: clusterIds.map((clusterId) => ({ id: clusterId })), + }, + }, + }); } export function createStage({ name }: { name: Stage['name'] }) { - return prisma.stage.create({ - data: { - name, - }, - }) + return prisma.stage.create({ + data: { + name, + }, + }); } export function updateStageName(id: Stage['id'], name: Stage['name']) { - return prisma.stage.update({ - where: { - id, - }, - data: { - name, - }, - }) + return prisma.stage.update({ + where: { + id, + }, + data: { + name, + }, + }); } export function deleteStage(id: Stage['id']) { - return prisma.stage.delete({ - where: { id }, - }) + return prisma.stage.delete({ + where: { id }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts index 3e1b6293b..62eed897a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts @@ -1,202 +1,273 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Stage } from '@cpn-console/shared' -import { stageContract } from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getUserMockInfos } from '../../utils/mocks.js' -import { BadRequest400 } from '../../utils/errors.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMock = vi.spyOn(business, 'listStages') -const businessGetEnvironmentsMock = vi.spyOn(business, 'getStageAssociatedEnvironments') -const businessCreateMock = vi.spyOn(business, 'createStage') -const businessUpdateMock = vi.spyOn(business, 'updateStage') -const businessDeleteMock = vi.spyOn(business, 'deleteStage') +import type { Stage } from '@cpn-console/shared'; +import { stageContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessListMock = vi.spyOn(business, 'listStages'); +const businessGetEnvironmentsMock = vi.spyOn( + business, + 'getStageAssociatedEnvironments', +); +const businessCreateMock = vi.spyOn(business, 'createStage'); +const businessUpdateMock = vi.spyOn(business, 'updateStage'); +const businessDeleteMock = vi.spyOn(business, 'deleteStage'); describe('test stageContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listStages', () => { - it('should return list of stages', async () => { - const stages = [] - businessListMock.mockResolvedValueOnce(stages) - - const response = await app.inject() - .get(stageContract.listStages.path) - .end() - - expect(businessListMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(stages) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('getStageEnvironments', () => { - it('should return stage environments for admin', async () => { - const environments = [] - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetEnvironmentsMock.mockResolvedValueOnce(environments) - const response = await app.inject() - .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(environments) - expect(response.statusCode).toEqual(200) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetEnvironmentsMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('createStage', () => { - const stage: Stage = { id: faker.string.uuid(), name: faker.string.alpha({ length: 5 }), clusterIds: [] } - - it('should create and return stage for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(stage) - const response = await app.inject() - .post(stageContract.createStage.path) - .body(stage) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(stage) - expect(response.statusCode).toEqual(201) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(stageContract.createStage.path) - .body(stage) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(stageContract.createStage.path) - .body(stage) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('updateStage', () => { - const stageId = faker.string.uuid() - const stage = { name: faker.string.alpha({ length: 5 }), clusterIds: [] } - - it('should update and return stage for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: stageId, ...stage }) - const response = await app.inject() - .put(stageContract.updateStage.path.replace(':stageId', stageId)) - .body(stage) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ id: stageId, ...stage }) - expect(response.statusCode).toEqual(200) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(stageContract.updateStage.path.replace(':stageId', stageId)) - .body(stage) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(stageContract.updateStage.path.replace(':stageId', stageId)) - .body(stage) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('deleteStage', () => { - it('should delete stage for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(1) - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('listStages', () => { + it('should return list of stages', async () => { + const stages = []; + businessListMock.mockResolvedValueOnce(stages); + + const response = await app + .inject() + .get(stageContract.listStages.path) + .end(); + + expect(businessListMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(stages); + expect(response.statusCode).toEqual(200); + }); + }); + + describe('getStageEnvironments', () => { + it('should return stage environments for admin', async () => { + const environments = []; + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessGetEnvironmentsMock.mockResolvedValueOnce(environments); + const response = await app + .inject() + .get( + stageContract.getStageEnvironments.path.replace( + ':stageId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(environments); + expect(response.statusCode).toEqual(200); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessGetEnvironmentsMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .get( + stageContract.getStageEnvironments.path.replace( + ':stageId', + faker.string.uuid(), + ), + ) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get( + stageContract.getStageEnvironments.path.replace( + ':stageId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('createStage', () => { + const stage: Stage = { + id: faker.string.uuid(), + name: faker.string.alpha({ length: 5 }), + clusterIds: [], + }; + + it('should create and return stage for admin', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce(stage); + const response = await app + .inject() + .post(stageContract.createStage.path) + .body(stage) + .end(); + + expect(businessCreateMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(stage); + expect(response.statusCode).toEqual(201); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .post(stageContract.createStage.path) + .body(stage) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(stageContract.createStage.path) + .body(stage) + .end(); + + expect(businessCreateMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('updateStage', () => { + const stageId = faker.string.uuid(); + const stage = { + name: faker.string.alpha({ length: 5 }), + clusterIds: [], + }; + + it('should update and return stage for admin', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce({ id: stageId, ...stage }); + const response = await app + .inject() + .put( + stageContract.updateStage.path.replace(':stageId', stageId), + ) + .body(stage) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual({ id: stageId, ...stage }); + expect(response.statusCode).toEqual(200); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .put( + stageContract.updateStage.path.replace(':stageId', stageId), + ) + .body(stage) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put( + stageContract.updateStage.path.replace(':stageId', stageId), + ) + .body(stage) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('deleteStage', () => { + it('should delete stage for admin', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce(null); + const response = await app + .inject() + .delete( + stageContract.deleteStage.path.replace( + ':stageId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessDeleteMock).toHaveBeenCalledTimes(1); + expect(response.body).toBeFalsy(); + expect(response.statusCode).toEqual(204); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .delete( + stageContract.deleteStage.path.replace( + ':stageId', + faker.string.uuid(), + ), + ) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + stageContract.deleteStage.path.replace( + ':stageId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessDeleteMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts index 710df2782..07ec2a55a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts @@ -1,88 +1,91 @@ -import { AdminAuthorized, stageContract } from '@cpn-console/shared' -import { - createStage, - deleteStage, - getStageAssociatedEnvironments, - listStages, - updateStage, -} from './business.js' -import { serverInstance } from '@old-server/app.js' +import { AdminAuthorized, stageContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' +import { + createStage, + deleteStage, + getStageAssociatedEnvironments, + listStages, + updateStage, +} from './business.js'; export function stageRouter() { - return serverInstance.router(stageContract, { - - // Récupérer les types d'environnement disponibles - listStages: async () => { - const body = await listStages() - - return { - status: 200, - body, - } - }, - - // Récupérer les environnements associés au stage - getStageEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const stageId = params.stageId - const body = await getStageAssociatedEnvironments(stageId) - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - // Créer un stage - createStage: async ({ request: req, body: data }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const body = await createStage(data) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - // Modifier une association stage / clusters - updateStage: async ({ request: req, params, body: data }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const stageId = params.stageId - - const body = await updateStage(stageId, data) - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - // Supprimer un stage - deleteStage: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const stageId = params.stageId - - const body = await deleteStage(stageId) - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - }) + return serverInstance.router(stageContract, { + // Récupérer les types d'environnement disponibles + listStages: async () => { + const body = await listStages(); + + return { + status: 200, + body, + }; + }, + + // Récupérer les environnements associés au stage + getStageEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + const body = await getStageAssociatedEnvironments(stageId); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Créer un stage + createStage: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const body = await createStage(data); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Modifier une association stage / clusters + updateStage: async ({ request: req, params, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + + const body = await updateStage(stageId, data); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Supprimer un stage + deleteStage: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + + const body = await deleteStage(stageId); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts index 35d484407..bcfeb5d9b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts @@ -1,22 +1,25 @@ -import { describe, expect, it } from 'vitest' -import prisma from '../../../__mocks__/prisma.js' -import { objToDb, updatePluginConfig } from './business.ts' +import { describe, expect, it } from 'vitest'; + +import prisma from '../../../__mocks__/prisma.js'; +import { objToDb, updatePluginConfig } from './business.ts'; describe('test system/config business', () => { - const config = { test: { key1: 'value1' } } - it('should transform object to db row', () => { - const response = objToDb({ test: { key1: 'value1' } }) - expect(response).toEqual([{ pluginName: 'test', key: 'key1', value: 'value1' }]) - }) - describe('updatePluginConfig', () => { - it('should update', async () => { - prisma.adminPlugin.upsert.mockResolvedValue(null) - await updatePluginConfig(config) - }) - it('should update 0 items cause missing manifest', async () => { - // @ts-ignore - await updatePluginConfig({ test: { key: 1 } }) - expect(prisma.adminPlugin.upsert).toHaveBeenCalledTimes(0) - }) - }) -}) + const config = { test: { key1: 'value1' } }; + it('should transform object to db row', () => { + const response = objToDb({ test: { key1: 'value1' } }); + expect(response).toEqual([ + { pluginName: 'test', key: 'key1', value: 'value1' }, + ]); + }); + describe('updatePluginConfig', () => { + it('should update', async () => { + prisma.adminPlugin.upsert.mockResolvedValue(null); + await updatePluginConfig(config); + }); + it('should update 0 items cause missing manifest', async () => { + // @ts-ignore + await updatePluginConfig({ test: { key: 1 } }); + expect(prisma.adminPlugin.upsert).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts index 86263d861..07dd6acca 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts @@ -1,50 +1,63 @@ -import type { - PluginsUpdateBody, -} from '@cpn-console/shared' -import { editStrippers, populatePluginManifests, servicesInfos } from '@cpn-console/hooks' import { - getAdminPlugin, - savePluginsConfig, -} from './queries.js' -import { BadRequest400 } from '@old-server/utils/errors.js' + editStrippers, + populatePluginManifests, + servicesInfos, +} from '@cpn-console/hooks'; +import type { PluginsUpdateBody } from '@cpn-console/shared'; +import { BadRequest400 } from '@old-server/utils/errors.js'; + +import { getAdminPlugin, savePluginsConfig } from './queries.js'; export type ConfigRecords = { - key: string - pluginName: string - value: string -}[] + key: string; + pluginName: string; + value: string; +}[]; export function objToDb(obj: PluginsUpdateBody): ConfigRecords { - return Object.entries(obj) - .map(([pluginName, values]) => Object.entries(values) - .map(([key, value]) => ({ pluginName, key, value }))) - .flat() + return Object.entries(obj) + .map(([pluginName, values]) => + Object.entries(values).map(([key, value]) => ({ + pluginName, + key, + value, + })), + ) + .flat(); } export async function getPluginsConfig() { - const globalConfig = await getAdminPlugin() + const globalConfig = await getAdminPlugin(); - return Object.values(servicesInfos).map(({ name, title, imgSrc, description }) => { - const manifest = populatePluginManifests({ - data: { - global: globalConfig, - }, - permissionTarget: 'admin', - pluginName: name, - select: { - global: true, - project: false, - }, - }) - return { imgSrc, title, name, manifest: manifest.global ?? [], description } - }).filter(plugin => plugin.manifest.length > 0) + return Object.values(servicesInfos) + .map(({ name, title, imgSrc, description }) => { + const manifest = populatePluginManifests({ + data: { + global: globalConfig, + }, + permissionTarget: 'admin', + pluginName: name, + select: { + global: true, + project: false, + }, + }); + return { + imgSrc, + title, + name, + manifest: manifest.global ?? [], + description, + }; + }) + .filter((plugin) => plugin.manifest.length > 0); } export async function updatePluginConfig(data: PluginsUpdateBody) { - const parsedData = editStrippers.global.safeParse(data) - if (!parsedData.success) return new BadRequest400(parsedData.error.message) - const records = objToDb(parsedData.data) + const parsedData = editStrippers.global.safeParse(data); + if (!parsedData.success) return new BadRequest400(parsedData.error.message); + const records = objToDb(parsedData.data); - await savePluginsConfig(records) - return null + await savePluginsConfig(records); + return null; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts index 5d56aeb47..a19db0fce 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts @@ -1,28 +1,29 @@ -import type { ConfigRecords } from './business.js' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; + +import type { ConfigRecords } from './business.js'; // CONFIG -export const getAdminPlugin = prisma.adminPlugin.findMany +export const getAdminPlugin = prisma.adminPlugin.findMany; export async function savePluginsConfig(records: ConfigRecords) { - for (const { pluginName, key, value } of records) { - await prisma.adminPlugin.upsert({ - create: { - pluginName, - key, - value: String(value), - }, - update: { - key, - value: String(value), - pluginName, - }, - where: { - pluginName_key: { - pluginName, - key, - }, - }, - }) - } + for (const { pluginName, key, value } of records) { + await prisma.adminPlugin.upsert({ + create: { + pluginName, + key, + value: String(value), + }, + update: { + key, + value: String(value), + pluginName, + }, + where: { + pluginName_key: { + pluginName, + key, + }, + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts index e1f11a105..b428692b2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts @@ -1,96 +1,111 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { systemPluginContract } from '@cpn-console/shared' -import app from '../../../app.js' -import * as utilsController from '../../../utils/controller.js' -import { getUserMockInfos } from '../../../utils/mocks.js' -import { BadRequest400 } from '../../../utils/errors.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetPluginsConfigMock = vi.spyOn(business, 'getPluginsConfig') -const businessUpdatePluginConfigMock = vi.spyOn(business, 'updatePluginConfig') +import { systemPluginContract } from '@cpn-console/shared'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../../app.js'; +import * as utilsController from '../../../utils/controller.js'; +import { BadRequest400 } from '../../../utils/errors.js'; +import { getUserMockInfos } from '../../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessGetPluginsConfigMock = vi.spyOn(business, 'getPluginsConfig'); +const businessUpdatePluginConfigMock = vi.spyOn(business, 'updatePluginConfig'); describe('test systemPluginContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('getPluginsConfig', () => { - it('should return plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true) - const pluginsConfig = [] - - authUserMock.mockResolvedValueOnce(user) - businessGetPluginsConfigMock.mockResolvedValueOnce(pluginsConfig) - - const response = await app.inject() - .get(systemPluginContract.getPluginsConfig.path) - .end() - - expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(pluginsConfig) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(systemPluginContract.getPluginsConfig.path) - .end() - - expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('updatePluginsConfig', () => { - const newConfig = { plugin1: { keyId: 'value' } } - it('should update plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessUpdatePluginConfigMock.mockResolvedValueOnce(newConfig) - - const response = await app.inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end() - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith(newConfig) - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end() - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - - it('should return error if business logic fails', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessUpdatePluginConfigMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - - const response = await app.inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end() - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith(newConfig) - expect(response.statusCode).toEqual(400) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('getPluginsConfig', () => { + it('should return plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true); + const pluginsConfig = []; + + authUserMock.mockResolvedValueOnce(user); + businessGetPluginsConfigMock.mockResolvedValueOnce(pluginsConfig); + + const response = await app + .inject() + .get(systemPluginContract.getPluginsConfig.path) + .end(); + + expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(pluginsConfig); + expect(response.statusCode).toEqual(200); + }); + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(systemPluginContract.getPluginsConfig.path) + .end(); + + expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('updatePluginsConfig', () => { + const newConfig = { plugin1: { keyId: 'value' } }; + it('should update plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessUpdatePluginConfigMock.mockResolvedValueOnce(newConfig); + + const response = await app + .inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end(); + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith( + newConfig, + ); + expect(response.statusCode).toEqual(204); + }); + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false); + + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end(); + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + + it('should return error if business logic fails', async () => { + const user = getUserMockInfos(true); + + authUserMock.mockResolvedValueOnce(user); + businessUpdatePluginConfigMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + + const response = await app + .inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end(); + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith( + newConfig, + ); + expect(response.statusCode).toEqual(400); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts index 2e4bffb11..d7a2218f5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts @@ -1,36 +1,38 @@ -import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared' -import { getPluginsConfig, updatePluginConfig } from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' +import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; -export function pluginConfigRouter() { - return serverInstance.router(systemPluginContract, { - // Récupérer les configurations plugins - getPluginsConfig: async ({ request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() +import { getPluginsConfig, updatePluginConfig } from './business.js'; - const services = await getPluginsConfig() +export function pluginConfigRouter() { + return serverInstance.router(systemPluginContract, { + // Récupérer les configurations plugins + getPluginsConfig: async ({ request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - return { - status: 200, - body: services, + const services = await getPluginsConfig(); - } - }, - // Mettre à jour les configurations plugins - updatePluginsConfig: async ({ request: req, body }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + return { + status: 200, + body: services, + }; + }, + // Mettre à jour les configurations plugins + updatePluginsConfig: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - const resBody = await updatePluginConfig(body) - if (resBody instanceof ErrorResType) return resBody + const resBody = await updatePluginConfig(body); + if (resBody instanceof ErrorResType) return resBody; - return { - status: 204, - body: resBody, - } - }, - }) + return { + status: 204, + body: resBody, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts index a45d7accc..5ef6bc555 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts @@ -1 +1 @@ -export * from './router.js' +export * from './router.js'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts index 321980ff5..5d8a97777 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts @@ -1,25 +1,33 @@ -import { describe, expect, it, vi } from 'vitest' -import { systemContract } from '@cpn-console/shared' -import app from '../../app.js' +import { systemContract } from '@cpn-console/shared'; +import { describe, expect, it, vi } from 'vitest'; -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) +import app from '../../app.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); describe('system - router', () => { - it('should send application version', async () => { - const response = await app.inject() - .get(systemContract.getVersion.path) - .end() + it('should send application version', async () => { + const response = await app + .inject() + .get(systemContract.getVersion.path) + .end(); - expect(response.statusCode).toBe(200) - expect(response.json()).toStrictEqual({ version: process.env.APP_VERSION || 'dev' }) - }) + expect(response.statusCode).toBe(200); + expect(response.json()).toStrictEqual({ + version: process.env.APP_VERSION || 'dev', + }); + }); - it('should send application health with status OK', async () => { - const response = await app.inject() - .get(systemContract.getHealth.path) - .end() + it('should send application health with status OK', async () => { + const response = await app + .inject() + .get(systemContract.getHealth.path) + .end(); - expect(response.statusCode).toBe(200) - expect(response.json()).toStrictEqual({ status: 'OK' }) - }) -}) + expect(response.statusCode).toBe(200); + expect(response.json()).toStrictEqual({ status: 'OK' }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts index 3ec017a3c..64dacc7c9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts @@ -1,21 +1,21 @@ -import { systemContract } from '@cpn-console/shared' -import { serverInstance } from '@old-server/app.js' -import { appVersion } from '@old-server/utils/env.js' +import { systemContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { appVersion } from '@old-server/utils/env.js'; export function systemRouter() { - return serverInstance.router(systemContract, { - getVersion: async () => ({ - status: 200, - body: { - version: appVersion, - }, - }), + return serverInstance.router(systemContract, { + getVersion: async () => ({ + status: 200, + body: { + version: appVersion, + }, + }), - getHealth: async () => ({ - status: 200, - body: { - status: 'OK', - }, - }), - }) + getHealth: async () => ({ + status: 200, + body: { + status: 'OK', + }, + }), + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts index 5e562353b..1b3376299 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts @@ -1,9 +1,13 @@ -import type { UpsertSystemSettingBody } from '@cpn-console/shared' +import type { UpsertSystemSettingBody } from '@cpn-console/shared'; + import { - getSystemSettings as getSystemSettingsQuery, - upsertSystemSetting as upsertSystemSettingQuery, -} from './queries.js' + getSystemSettings as getSystemSettingsQuery, + upsertSystemSetting as upsertSystemSettingQuery, +} from './queries.js'; -export const getSystemSettings = (key?: string) => getSystemSettingsQuery({ key }) +export const getSystemSettings = (key?: string) => + getSystemSettingsQuery({ key }); -export const upsertSystemSetting = (newSystemSetting: UpsertSystemSettingBody) => upsertSystemSettingQuery(newSystemSetting) +export const upsertSystemSetting = ( + newSystemSetting: UpsertSystemSettingBody, +) => upsertSystemSettingQuery(newSystemSetting); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts index 15b352288..08e72e84c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts @@ -1,18 +1,19 @@ -import type { Prisma, SystemSetting } from '@prisma/client' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { Prisma, SystemSetting } from '@prisma/client'; export function upsertSystemSetting(newSystemSetting: SystemSetting) { - return prisma.systemSetting.upsert({ - create: { - ...newSystemSetting, - }, - update: { - value: newSystemSetting.value, - }, - where: { - key: newSystemSetting.key, - }, - }) + return prisma.systemSetting.upsert({ + create: { + ...newSystemSetting, + }, + update: { + value: newSystemSetting.value, + }, + where: { + key: newSystemSetting.key, + }, + }); } -export const getSystemSettings = (where?: Prisma.SystemSettingWhereInput) => prisma.systemSetting.findMany({ where }) +export const getSystemSettings = (where?: Prisma.SystemSettingWhereInput) => + prisma.systemSetting.findMany({ where }); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts index c4c6b56b6..679c3b07a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts @@ -1,67 +1,79 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { systemSettingsContract } from '@cpn-console/shared' -import app from '../../../app.js' -import * as utilsController from '../../../utils/controller.js' -import { getUserMockInfos } from '../../../utils/mocks.js' -import * as business from './business.js' +import { systemSettingsContract } from '@cpn-console/shared'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('fastify-keycloak-adapter', (await import('../../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetSystemSettingsMock = vi.spyOn(business, 'getSystemSettings') -const businessUpsertSystemSettingMock = vi.spyOn(business, 'upsertSystemSetting') +import app from '../../../app.js'; +import * as utilsController from '../../../utils/controller.js'; +import { getUserMockInfos } from '../../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessGetSystemSettingsMock = vi.spyOn(business, 'getSystemSettings'); +const businessUpsertSystemSettingMock = vi.spyOn( + business, + 'upsertSystemSetting', +); describe('test systemSettingsContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) + beforeEach(() => { + vi.resetAllMocks(); + }); - describe('listSystemSettings', () => { - it('should return plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true) - const systemSettings = [] + describe('listSystemSettings', () => { + it('should return plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true); + const systemSettings = []; - authUserMock.mockResolvedValueOnce(user) - businessGetSystemSettingsMock.mockResolvedValueOnce(systemSettings) + authUserMock.mockResolvedValueOnce(user); + businessGetSystemSettingsMock.mockResolvedValueOnce(systemSettings); - const response = await app.inject() - .get(systemSettingsContract.listSystemSettings.path) - .end() + const response = await app + .inject() + .get(systemSettingsContract.listSystemSettings.path) + .end(); - expect(businessGetSystemSettingsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(systemSettings) - expect(response.statusCode).toEqual(200) - }) - }) + expect(businessGetSystemSettingsMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(systemSettings); + expect(response.statusCode).toEqual(200); + }); + }); - describe('upsertSystemSetting', () => { - const newConfig = { key: 'key1', value: 'value1' } - it('should update system setting, authorized users', async () => { - const user = getUserMockInfos(true) + describe('upsertSystemSetting', () => { + const newConfig = { key: 'key1', value: 'value1' }; + it('should update system setting, authorized users', async () => { + const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user) - businessUpsertSystemSettingMock.mockResolvedValueOnce(newConfig) + authUserMock.mockResolvedValueOnce(user); + businessUpsertSystemSettingMock.mockResolvedValueOnce(newConfig); - const response = await app.inject() - .post(systemSettingsContract.upsertSystemSetting.path) - .body(newConfig) - .end() + const response = await app + .inject() + .post(systemSettingsContract.upsertSystemSetting.path) + .body(newConfig) + .end(); - expect(businessUpsertSystemSettingMock).toHaveBeenCalledWith(newConfig) - expect(response.statusCode).toEqual(201) - }) + expect(businessUpsertSystemSettingMock).toHaveBeenCalledWith( + newConfig, + ); + expect(response.statusCode).toEqual(201); + }); - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user) + authUserMock.mockResolvedValueOnce(user); - const response = await app.inject() - .post(systemSettingsContract.upsertSystemSetting.path) - .body(newConfig) - .end() + const response = await app + .inject() + .post(systemSettingsContract.upsertSystemSetting.path) + .body(newConfig) + .end(); - expect(businessUpsertSystemSettingMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) + expect(businessUpsertSystemSettingMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts index b2c1114fc..99a094c01 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts @@ -1,30 +1,32 @@ -import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared' -import { getSystemSettings, upsertSystemSetting } from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { Forbidden403 } from '@old-server/utils/errors.js' +import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { Forbidden403 } from '@old-server/utils/errors.js'; + +import { getSystemSettings, upsertSystemSetting } from './business.js'; export function systemSettingsRouter() { - return serverInstance.router(systemSettingsContract, { - listSystemSettings: async ({ query }) => { - const systemSettings = await getSystemSettings(query.key) + return serverInstance.router(systemSettingsContract, { + listSystemSettings: async ({ query }) => { + const systemSettings = await getSystemSettings(query.key); - return { - status: 200, - body: systemSettings, - } - }, + return { + status: 200, + body: systemSettings, + }; + }, - upsertSystemSetting: async ({ request: req, body: data }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + upsertSystemSetting: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - const systemSetting = await upsertSystemSetting(data) + const systemSetting = await upsertSystemSetting(data); - return { - status: 201, - body: systemSetting, - } - }, - }) + return { + status: 201, + body: systemSetting, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts index 50e1dd20c..8f73f72da 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts @@ -1,222 +1,255 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import prisma from '../../__mocks__/prisma.js' -import type { UserDetails } from '../../types/index.ts' -import { TokenInvalidReason, getMatchingUsers, getUsers, logViaSession, logViaToken, patchUsers } from './business.ts' -import * as queries from './queries.js' - -const getUsersQueryMock = vi.spyOn(queries, 'getUsers') -const getMatchingUsersQueryMock = vi.spyOn(queries, 'getMatchingUsers') +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import prisma from '../../__mocks__/prisma.js'; +import type { UserDetails } from '../../types/index.ts'; +import { + TokenInvalidReason, + getMatchingUsers, + getUsers, + logViaSession, + logViaToken, + patchUsers, +} from './business.ts'; +import * as queries from './queries.js'; + +const getUsersQueryMock = vi.spyOn(queries, 'getUsers'); +const getMatchingUsersQueryMock = vi.spyOn(queries, 'getMatchingUsers'); describe('test users business', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const user = { - adminRoleIds: [], - createdAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - id: faker.string.uuid(), - lastName: faker.person.lastName(), - updatedAt: new Date(), - } - const projectId = faker.string.uuid() - const adminRoleId = faker.string.uuid() - describe('patchUsers', () => { - it('should do nothing', async () => { - prisma.user.update.mockResolvedValue(null) - - await patchUsers([]) - - expect(prisma.user.update).toHaveBeenCalledTimes(0) - }) - - it('should update a user adminRoleIds', async () => { - const userUpdated = { id: user.id, adminRoleIds: user.adminRoleIds } - - prisma.user.update.mockResolvedValue(user) - - prisma.user.findMany.mockResolvedValue([]) - - await patchUsers([userUpdated]) - expect(prisma.user.update).toHaveBeenCalledTimes(1) - expect(prisma.user.findMany).toHaveBeenCalledTimes(1) - - await patchUsers([userUpdated, userUpdated]) - expect(prisma.user.update).toHaveBeenCalledTimes(3) - }) - }) - describe('getUsers', () => { - it('should query without where', async () => { - prisma.user.update.mockResolvedValue(null) - - await getUsers({}) - - expect(getUsersQueryMock).toHaveBeenCalledTimes(1) - expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [] }) - }) - it('should query with filter adminRoleIds', async () => { - prisma.user.update.mockResolvedValue(null) - - await getUsers({ adminRoleIds: [adminRoleId] }) - - expect(getUsersQueryMock).toHaveBeenCalledTimes(1) - expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [{ adminRoleIds: { hasEvery: [adminRoleId] } }] }) - }) - }) - - describe('getMatchingUsers', () => { - const AND = [ - { - OR: [ - { - email: { - contains: 'abc', - mode: 'insensitive', + beforeEach(() => { + vi.resetAllMocks(); + }); + + const user = { + adminRoleIds: [], + createdAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + id: faker.string.uuid(), + lastName: faker.person.lastName(), + updatedAt: new Date(), + }; + const projectId = faker.string.uuid(); + const adminRoleId = faker.string.uuid(); + describe('patchUsers', () => { + it('should do nothing', async () => { + prisma.user.update.mockResolvedValue(null); + + await patchUsers([]); + + expect(prisma.user.update).toHaveBeenCalledTimes(0); + }); + + it('should update a user adminRoleIds', async () => { + const userUpdated = { + id: user.id, + adminRoleIds: user.adminRoleIds, + }; + + prisma.user.update.mockResolvedValue(user); + + prisma.user.findMany.mockResolvedValue([]); + + await patchUsers([userUpdated]); + expect(prisma.user.update).toHaveBeenCalledTimes(1); + expect(prisma.user.findMany).toHaveBeenCalledTimes(1); + + await patchUsers([userUpdated, userUpdated]); + expect(prisma.user.update).toHaveBeenCalledTimes(3); + }); + }); + describe('getUsers', () => { + it('should query without where', async () => { + prisma.user.update.mockResolvedValue(null); + + await getUsers({}); + + expect(getUsersQueryMock).toHaveBeenCalledTimes(1); + expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [] }); + }); + it('should query with filter adminRoleIds', async () => { + prisma.user.update.mockResolvedValue(null); + + await getUsers({ adminRoleIds: [adminRoleId] }); + + expect(getUsersQueryMock).toHaveBeenCalledTimes(1); + expect(getUsersQueryMock).toHaveBeenCalledWith({ + AND: [{ adminRoleIds: { hasEvery: [adminRoleId] } }], + }); + }); + }); + + describe('getMatchingUsers', () => { + const AND = [ + { + OR: [ + { + email: { + contains: 'abc', + mode: 'insensitive', + }, + }, + { + firstName: { + contains: 'abc', + mode: 'insensitive', + }, + }, + { + lastName: { + contains: 'abc', + mode: 'insensitive', + }, + }, + ], + }, + { + type: 'human', }, - }, - { - firstName: { - contains: 'abc', - mode: 'insensitive', + ]; + it('should query only with letters ', async () => { + prisma.user.update.mockResolvedValue(null); + + await getMatchingUsers({ letters: 'abc' }); + + expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1); + expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND }); + }); + it('should query with letters and projectId', async () => { + prisma.user.update.mockResolvedValue(null); + + await getMatchingUsers({ + letters: 'abc', + notInProjectId: projectId, + }); + + expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1); + expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ + AND: [ + { + projectMembers: { + none: { + projectId, + }, + }, + }, + { + projectsOwned: { + none: { + id: projectId, + }, + }, + }, + ].concat(AND), + }); + }); + }); + describe('logViaSession', () => { + // ça ne teste pas tout mais c'est déjà bien hein + const adminRoles = [ + { + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: 0n, + position: 0, }, - }, - { - lastName: { - contains: 'abc', - mode: 'insensitive', + { + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '/admin', + permissions: 0n, + position: 0, }, - }, - ], - }, - { - type: 'human', - }, - ] - it('should query only with letters ', async () => { - prisma.user.update.mockResolvedValue(null) - - await getMatchingUsers({ letters: 'abc' }) - - expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1) - expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND }) - }) - it('should query with letters and projectId', async () => { - prisma.user.update.mockResolvedValue(null) - - await getMatchingUsers({ letters: 'abc', notInProjectId: projectId }) - - expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1) - expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND: [{ - projectMembers: { - none: { - projectId, - }, - }, - }, { - projectsOwned: { - none: { - id: projectId, - }, - }, - }].concat(AND) }) - }) - }) - describe('logViaSession', () => { - // ça ne teste pas tout mais c'est déjà bien hein - const adminRoles = [{ - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: 0n, - position: 0, - }, { - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '/admin', - permissions: 0n, - position: 0, - }] - const userToLog: UserDetails = { - id: faker.string.uuid(), - email: user.email, - firstName: user.firstName, - groups: [], - lastName: user.lastName, - } - it('should create user and return adminPerms', async () => { - prisma.adminRole.findMany.mockResolvedValue(adminRoles) - prisma.user.findUnique.mockResolvedValue(undefined) - prisma.user.create.mockResolvedValue(user) - prisma.user.update.mockResolvedValue(user) - const response = await logViaSession(userToLog) - expect(response.adminPerms).toBe(0n) - expect(prisma.user.create).toHaveBeenCalledTimes(1) - }) - it('should update user and return adminPerms', async () => { - prisma.adminRole.findMany.mockResolvedValue(adminRoles) - prisma.user.findUnique.mockResolvedValue(user) - prisma.user.update.mockResolvedValue(user) - const response = await logViaSession(userToLog) - expect(response.adminPerms).toEqual(0n) - expect(prisma.user.create).toHaveBeenCalledTimes(0) - }) - }) -}) + ]; + const userToLog: UserDetails = { + id: faker.string.uuid(), + email: user.email, + firstName: user.firstName, + groups: [], + lastName: user.lastName, + }; + it('should create user and return adminPerms', async () => { + prisma.adminRole.findMany.mockResolvedValue(adminRoles); + prisma.user.findUnique.mockResolvedValue(undefined); + prisma.user.create.mockResolvedValue(user); + prisma.user.update.mockResolvedValue(user); + const response = await logViaSession(userToLog); + expect(response.adminPerms).toBe(0n); + expect(prisma.user.create).toHaveBeenCalledTimes(1); + }); + it('should update user and return adminPerms', async () => { + prisma.adminRole.findMany.mockResolvedValue(adminRoles); + prisma.user.findUnique.mockResolvedValue(user); + prisma.user.update.mockResolvedValue(user); + const response = await logViaSession(userToLog); + expect(response.adminPerms).toEqual(0n); + expect(prisma.user.create).toHaveBeenCalledTimes(0); + }); + }); +}); describe('logViaToken', () => { - const nextYear = new Date() - const lastYear = new Date() - nextYear.setFullYear((new Date()).getFullYear() + 1) - lastYear.setFullYear((new Date()).getFullYear() - 1) - const baseToken = { - createdAt: new Date(), - hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', - id: faker.string.uuid(), - lastUse: null, - permissions: 2n, - userId: null, - status: 'active', - } as const - - it('should return identity', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken }) - const identity = await logViaToken('test') - expect(identity.adminPerms).toBe(2n) - }) - - it('should return identity based on pat', async () => { - const pat = structuredClone(baseToken) - delete pat.permissions - pat.owner = { adminRoleIds: null } - prisma.personalAccessToken.findFirst.mockResolvedValueOnce(pat) - const identity = await logViaToken('test') - expect(identity.adminPerms).toBe(0n) - }) - - it('should return identity, with expirationDate', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, expirationDate: nextYear }) - const identity = await logViaToken('test') - expect(identity.adminPerms).toBe(2n) - }) - - it('should return cause revoked', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, status: 'revoked' }) - const identity = await logViaToken('test') - expect(identity).toBe(TokenInvalidReason.INACTIVE) - }) - - it('should return cause expired', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, expirationDate: lastYear }) - const identity = await logViaToken('test') - expect(identity).toBe(TokenInvalidReason.EXPIRED) - }) - - it('should return cause not found', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce(undefined) - const identity = await logViaToken('test') - expect(identity).toBe(TokenInvalidReason.NOT_FOUND) - }) -}) + const nextYear = new Date(); + const lastYear = new Date(); + nextYear.setFullYear(new Date().getFullYear() + 1); + lastYear.setFullYear(new Date().getFullYear() - 1); + const baseToken = { + createdAt: new Date(), + hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + id: faker.string.uuid(), + lastUse: null, + permissions: 2n, + userId: null, + status: 'active', + } as const; + + it('should return identity', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken }); + const identity = await logViaToken('test'); + expect(identity.adminPerms).toBe(2n); + }); + + it('should return identity based on pat', async () => { + const pat = structuredClone(baseToken); + delete pat.permissions; + pat.owner = { adminRoleIds: null }; + prisma.personalAccessToken.findFirst.mockResolvedValueOnce(pat); + const identity = await logViaToken('test'); + expect(identity.adminPerms).toBe(0n); + }); + + it('should return identity, with expirationDate', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ + ...baseToken, + expirationDate: nextYear, + }); + const identity = await logViaToken('test'); + expect(identity.adminPerms).toBe(2n); + }); + + it('should return cause revoked', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ + ...baseToken, + status: 'revoked', + }); + const identity = await logViaToken('test'); + expect(identity).toBe(TokenInvalidReason.INACTIVE); + }); + + it('should return cause expired', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ + ...baseToken, + expirationDate: lastYear, + }); + const identity = await logViaToken('test'); + expect(identity).toBe(TokenInvalidReason.EXPIRED); + }); + + it('should return cause not found', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce(undefined); + const identity = await logViaToken('test'); + expect(identity).toBe(TokenInvalidReason.NOT_FOUND); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts index 052d4f92f..40fa43e9a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts @@ -1,201 +1,295 @@ -import { createHash } from 'node:crypto' -import type { AdminRole, AdminToken, PersonalAccessToken, Prisma, User } from '@prisma/client' -import type { XOR, userContract } from '@cpn-console/shared' -import { getMatchingUsers as getMatchingUsersQuery, getUsers as getUsersQuery } from '@old-server/resources/queries-index.js' -import prisma from '@old-server/prisma.js' -import type { UserDetails } from '@old-server/types/index.js' -import { BadRequest400 } from '@old-server/utils/errors.js' - -export async function getUsers(query: typeof userContract.getAllUsers.query._type, relationType: 'OR' | 'AND' = 'AND') { - const whereInputs: Prisma.UserWhereInput[] = [] - if (query.adminRoleIds?.length) { - whereInputs.push({ adminRoleIds: { hasEvery: query.adminRoleIds } }) - } - if (query.adminRoles?.length) { - const roles = query.adminRoles - ? await prisma.adminRole.findMany({ where: { name: { in: query.adminRoles } } }) - : [] - - const adminRoleNameNotFound = query.adminRoles?.find(nameQueried => !roles.find(({ name }) => name === nameQueried)) - if (adminRoleNameNotFound) { - return new BadRequest400(`Unable to find adminRole ${adminRoleNameNotFound}`) - } - whereInputs.push({ adminRoleIds: { hasEvery: roles.map(({ id }) => id) } }) - } - if (query.memberOfIds) { - whereInputs.push({ - AND: query.memberOfIds.map(id => ({ - OR: [ - { projectsOwned: { some: { id } } }, - { ProjectMembers: { some: { project: { id } } } }, - ], - })), - }) - } - - return getUsersQuery({ [relationType]: whereInputs }) +import type { XOR, userContract } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; +import { + getMatchingUsers as getMatchingUsersQuery, + getUsers as getUsersQuery, +} from '@old-server/resources/queries-index.js'; +import type { UserDetails } from '@old-server/types/index.js'; +import { BadRequest400 } from '@old-server/utils/errors.js'; +import type { + AdminRole, + AdminToken, + PersonalAccessToken, + Prisma, + User, +} from '@prisma/client'; +import { createHash } from 'node:crypto'; + +export async function getUsers( + query: typeof userContract.getAllUsers.query._type, + relationType: 'OR' | 'AND' = 'AND', +) { + const whereInputs: Prisma.UserWhereInput[] = []; + if (query.adminRoleIds?.length) { + whereInputs.push({ adminRoleIds: { hasEvery: query.adminRoleIds } }); + } + if (query.adminRoles?.length) { + const roles = query.adminRoles + ? await prisma.adminRole.findMany({ + where: { name: { in: query.adminRoles } }, + }) + : []; + + const adminRoleNameNotFound = query.adminRoles?.find( + (nameQueried) => !roles.find(({ name }) => name === nameQueried), + ); + if (adminRoleNameNotFound) { + return new BadRequest400( + `Unable to find adminRole ${adminRoleNameNotFound}`, + ); + } + whereInputs.push({ + adminRoleIds: { hasEvery: roles.map(({ id }) => id) }, + }); + } + if (query.memberOfIds) { + whereInputs.push({ + AND: query.memberOfIds.map((id) => ({ + OR: [ + { projectsOwned: { some: { id } } }, + { ProjectMembers: { some: { project: { id } } } }, + ], + })), + }); + } + + return getUsersQuery({ [relationType]: whereInputs }); } -export async function getMatchingUsers(query: typeof userContract.getMatchingUsers.query._type) { - const AND: Prisma.UserWhereInput[] = [] - if (query.notInProjectId) { - AND.push({ projectMembers: { none: { projectId: query.notInProjectId } } }) - AND.push({ projectsOwned: { none: { id: query.notInProjectId } } }) - } - const filter = { contains: query.letters, mode: 'insensitive' } as const // Default value: default - if (query.letters) { - AND.push({ - OR: [{ - email: filter, - }, { - firstName: filter, - }, { - lastName: filter, - }], - }) - AND.push({ type: 'human' }) - } - - return getMatchingUsersQuery({ - AND, - }) +export async function getMatchingUsers( + query: typeof userContract.getMatchingUsers.query._type, +) { + const AND: Prisma.UserWhereInput[] = []; + if (query.notInProjectId) { + AND.push({ + projectMembers: { none: { projectId: query.notInProjectId } }, + }); + AND.push({ projectsOwned: { none: { id: query.notInProjectId } } }); + } + const filter = { contains: query.letters, mode: 'insensitive' } as const; // Default value: default + if (query.letters) { + AND.push({ + OR: [ + { + email: filter, + }, + { + firstName: filter, + }, + { + lastName: filter, + }, + ], + }); + AND.push({ type: 'human' }); + } + + return getMatchingUsersQuery({ + AND, + }); } -export async function patchUsers(users: typeof userContract.patchUsers.body._type) { - for (const user of users) { - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - adminRoleIds: user.adminRoleIds, - }, - }) - } - - return prisma.user.findMany({ - where: { - id: { in: users.map(({ id }) => id) }, - }, - }) +export async function patchUsers( + users: typeof userContract.patchUsers.body._type, +) { + for (const user of users) { + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + adminRoleIds: user.adminRoleIds, + }, + }); + } + + return prisma.user.findMany({ + where: { + id: { in: users.map(({ id }) => id) }, + }, + }); } export enum TokenInvalidReason { - INACTIVE = 'Not active', - EXPIRED = 'Expired', - NOT_FOUND = 'Not authenticated', + INACTIVE = 'Not active', + EXPIRED = 'Expired', + NOT_FOUND = 'Not authenticated', } -type UserTrial = Omit -export async function logViaSession({ id, email, groups, ...user }: UserTrial): Promise<{ user: User, adminPerms: bigint }> { - let userDb = await prisma.user.findUnique({ - where: { id }, - }) - - if (!userDb) { - userDb = await prisma.user.create({ data: { email, id, ...user, adminRoleIds: [], type: 'human' } }) - } - - const matchingAdminRoles = await prisma.adminRole.findMany({ - where: { OR: [{ oidcGroup: { in: groups } }, { id: { in: userDb.adminRoleIds } }] }, - }) - - const oidcRoleIds = matchingAdminRoles - .filter(({ oidcGroup }) => oidcGroup && groups.includes(oidcGroup)) - .map(({ id }) => id) - - const nonOidcRoleIds = matchingAdminRoles - .filter(({ oidcGroup, id }) => !oidcGroup && userDb.adminRoleIds.includes(id)) - .map(({ id }) => id) - - // On enregistre en bdd uniquement les roles de l'utilisateur - // qui ne viennent pas de keycloak - const updatedUser = await prisma.user.update({ where: { id }, data: { ...user, adminRoleIds: nonOidcRoleIds, lastLogin: (new Date()).toISOString() } }) - .then(user => ({ ...user, adminRoleIds: [...user.adminRoleIds, ...oidcRoleIds] })) - return { - user: updatedUser, - adminPerms: sumAdminPerms(matchingAdminRoles), - } +type UserTrial = Omit; +export async function logViaSession({ + id, + email, + groups, + ...user +}: UserTrial): Promise<{ user: User; adminPerms: bigint }> { + let userDb = await prisma.user.findUnique({ + where: { id }, + }); + + if (!userDb) { + userDb = await prisma.user.create({ + data: { email, id, ...user, adminRoleIds: [], type: 'human' }, + }); + } + + const matchingAdminRoles = await prisma.adminRole.findMany({ + where: { + OR: [ + { oidcGroup: { in: groups } }, + { id: { in: userDb.adminRoleIds } }, + ], + }, + }); + + const oidcRoleIds = matchingAdminRoles + .filter(({ oidcGroup }) => oidcGroup && groups.includes(oidcGroup)) + .map(({ id }) => id); + + const nonOidcRoleIds = matchingAdminRoles + .filter( + ({ oidcGroup, id }) => + !oidcGroup && userDb.adminRoleIds.includes(id), + ) + .map(({ id }) => id); + + // On enregistre en bdd uniquement les roles de l'utilisateur + // qui ne viennent pas de keycloak + const updatedUser = await prisma.user + .update({ + where: { id }, + data: { + ...user, + adminRoleIds: nonOidcRoleIds, + lastLogin: new Date().toISOString(), + }, + }) + .then((user) => ({ + ...user, + adminRoleIds: [...user.adminRoleIds, ...oidcRoleIds], + })); + return { + user: updatedUser, + adminPerms: sumAdminPerms(matchingAdminRoles), + }; } -type UserWithTokenId = Omit & { tokenId: string } -export async function logViaToken(pass: string): Promise<({ user: UserWithTokenId, adminPerms: bigint }) | TokenInvalidReason> { - const passHash = createHash('sha256').update(pass).digest('hex') - - let token: (XOR & { owner: User }) | TokenInvalidReason | undefined - const tokenLoginMethods = [findPersonalAccessToken, findAdminToken] - for (const tokenLoginMethod of tokenLoginMethods) { - token = await tokenLoginMethod(passHash) - if (token) { - break - } - } - - if (typeof token === 'string') { - return token - } - if (!token) { - return TokenInvalidReason.NOT_FOUND - } - - return { - user: { - ...token.owner, - tokenId: token.id, - }, - adminPerms: token?.permissions ?? await getAdminRolesAndSum(token.owner.adminRoleIds), - } +type UserWithTokenId = Omit & { tokenId: string }; +export async function logViaToken( + pass: string, +): Promise<{ user: UserWithTokenId; adminPerms: bigint } | TokenInvalidReason> { + const passHash = createHash('sha256').update(pass).digest('hex'); + + let token: + | (XOR & { owner: User }) + | TokenInvalidReason + | undefined; + const tokenLoginMethods = [findPersonalAccessToken, findAdminToken]; + for (const tokenLoginMethod of tokenLoginMethods) { + token = await tokenLoginMethod(passHash); + if (token) { + break; + } + } + + if (typeof token === 'string') { + return token; + } + if (!token) { + return TokenInvalidReason.NOT_FOUND; + } + + return { + user: { + ...token.owner, + tokenId: token.id, + }, + adminPerms: + token?.permissions ?? + (await getAdminRolesAndSum(token.owner.adminRoleIds)), + }; } -function isTokenInvalid(token: AdminToken | PersonalAccessToken): TokenInvalidReason | undefined { - if (token.status !== 'active') { - return TokenInvalidReason.INACTIVE - } - const currentDate = new Date() - if (token.expirationDate && currentDate.getTime() > token.expirationDate?.getTime()) { - return TokenInvalidReason.EXPIRED - } +function isTokenInvalid( + token: AdminToken | PersonalAccessToken, +): TokenInvalidReason | undefined { + if (token.status !== 'active') { + return TokenInvalidReason.INACTIVE; + } + const currentDate = new Date(); + if ( + token.expirationDate && + currentDate.getTime() > token.expirationDate?.getTime() + ) { + return TokenInvalidReason.EXPIRED; + } } function sumAdminPerms(roles: AdminRole[]): bigint { - if (!roles.length) { - return 0n - } - return roles.reduce((acc, curr) => acc | curr.permissions, 0n) + if (!roles.length) { + return 0n; + } + return roles.reduce((acc, curr) => acc | curr.permissions, 0n); } -async function getAdminRolesAndSum(roles: AdminRole['id'][] | null): Promise { - if (!roles?.length) { - return 0n - } - return sumAdminPerms(await prisma.adminRole.findMany({ - where: { id: { in: roles } }, - })) +async function getAdminRolesAndSum( + roles: AdminRole['id'][] | null, +): Promise { + if (!roles?.length) { + return 0n; + } + return sumAdminPerms( + await prisma.adminRole.findMany({ + where: { id: { in: roles } }, + }), + ); } // List all token tpe authentication -async function findPersonalAccessToken(digest: string): Promise<(PersonalAccessToken & { owner: User }) | undefined | TokenInvalidReason> { - const token = await prisma.personalAccessToken.findFirst({ where: { hash: digest }, include: { owner: true } }) - if (!token) - return undefined - const invalidReason = isTokenInvalid(token) - if (invalidReason) { - return invalidReason - } - await prisma.personalAccessToken.update({ where: { id: token.id }, data: { lastUse: (new Date()).toISOString() } }) - await prisma.user.update({ where: { id: token.owner.id }, data: { lastLogin: (new Date()).toISOString() } }) - return token +async function findPersonalAccessToken( + digest: string, +): Promise< + (PersonalAccessToken & { owner: User }) | undefined | TokenInvalidReason +> { + const token = await prisma.personalAccessToken.findFirst({ + where: { hash: digest }, + include: { owner: true }, + }); + if (!token) return undefined; + const invalidReason = isTokenInvalid(token); + if (invalidReason) { + return invalidReason; + } + await prisma.personalAccessToken.update({ + where: { id: token.id }, + data: { lastUse: new Date().toISOString() }, + }); + await prisma.user.update({ + where: { id: token.owner.id }, + data: { lastLogin: new Date().toISOString() }, + }); + return token; } -async function findAdminToken(digest: string): Promise<(AdminToken & { owner: User }) | undefined | TokenInvalidReason> { - const token = await prisma.adminToken.findFirst({ where: { hash: digest }, include: { owner: true } }) - if (!token) - return undefined - const invalidReason = isTokenInvalid(token) - if (invalidReason) { - return invalidReason - } - await prisma.adminToken.update({ where: { id: token.id }, data: { lastUse: (new Date()).toISOString() } }) - await prisma.user.update({ where: { id: token.userId }, data: { lastLogin: (new Date()).toISOString() } }) - return token +async function findAdminToken( + digest: string, +): Promise<(AdminToken & { owner: User }) | undefined | TokenInvalidReason> { + const token = await prisma.adminToken.findFirst({ + where: { hash: digest }, + include: { owner: true }, + }); + if (!token) return undefined; + const invalidReason = isTokenInvalid(token); + if (invalidReason) { + return invalidReason; + } + await prisma.adminToken.update({ + where: { id: token.id }, + data: { lastUse: new Date().toISOString() }, + }); + await prisma.user.update({ + where: { id: token.userId }, + data: { lastLogin: new Date().toISOString() }, + }); + return token; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts index fe2559c79..2b8eae2fb 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts @@ -1,60 +1,83 @@ -import type { Prisma, User } from '@prisma/client' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { Prisma, User } from '@prisma/client'; -type UserCreate = Omit +type UserCreate = Omit; // SELECT -export const getUsers = (where?: Prisma.UserWhereInput) => prisma.user.findMany({ where }) +export const getUsers = (where?: Prisma.UserWhereInput) => + prisma.user.findMany({ where }); export async function getUserInfos(id: User['id']) { - return prisma.user.findMany({ - where: { id }, - include: { - logs: true, - }, - }) + return prisma.user.findMany({ + where: { id }, + include: { + logs: true, + }, + }); } export function getMatchingUsers(where: Prisma.UserWhereInput) { - return prisma.user.findMany({ - where, - take: 5, - }) + return prisma.user.findMany({ + where, + take: 5, + }); } export function getUserById(id: User['id']) { - return prisma.user.findUnique({ where: { id } }) + return prisma.user.findUnique({ where: { id } }); } export function getUserOrThrow(id: User['id']) { - return prisma.user.findUniqueOrThrow({ - where: { id }, - }) + return prisma.user.findUniqueOrThrow({ + where: { id }, + }); } export function getUserByEmail(email: User['email']) { - return prisma.user.findUnique({ where: { email } }) + return prisma.user.findUnique({ where: { email } }); } // CREATE -export async function createUser({ id, email, firstName, lastName, type }: UserCreate) { - const user = await getUserByEmail(email) - if (user) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') - return prisma.user.create({ data: { id, email, firstName, lastName, type } }) +export async function createUser({ + id, + email, + firstName, + lastName, + type, +}: UserCreate) { + const user = await getUserByEmail(email); + if (user) + throw new Error('Un utilisateur avec cette adresse e-mail existe déjà'); + return prisma.user.create({ + data: { id, email, firstName, lastName, type }, + }); } // UPDATE -export async function updateUserById({ id, email, firstName, lastName }: UserCreate) { - const user = await getUserById(id) - const isEmailAlreadyTaken = await getUserByEmail(email) - if (!user) throw new Error('L\'utilisateur demandé n\'existe pas') - if (isEmailAlreadyTaken) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') - if (user && !isEmailAlreadyTaken) { - return prisma.user.update({ where: { id }, data: { email, firstName, lastName } }) - } +export async function updateUserById({ + id, + email, + firstName, + lastName, +}: UserCreate) { + const user = await getUserById(id); + const isEmailAlreadyTaken = await getUserByEmail(email); + if (!user) throw new Error("L'utilisateur demandé n'existe pas"); + if (isEmailAlreadyTaken) + throw new Error('Un utilisateur avec cette adresse e-mail existe déjà'); + if (user && !isEmailAlreadyTaken) { + return prisma.user.update({ + where: { id }, + data: { email, firstName, lastName }, + }); + } } // TECH export function _createUser(data: Prisma.UserCreateInput) { - return prisma.user.upsert({ where: { id: data.id }, create: data, update: data }) + return prisma.user.upsert({ + where: { id: data.id }, + create: data, + update: data, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts index ffa817afe..3c21c1073 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts @@ -1,139 +1,156 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { userContract } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getUserMockInfos, setRequestor } from '../../utils/mocks.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetMatchingMock = vi.spyOn(business, 'getMatchingUsers') -const businessLogViaSessionMock = vi.spyOn(business, 'logViaSession') -const businessGetUsersMock = vi.spyOn(business, 'getUsers') -const businessPatchMock = vi.spyOn(business, 'patchUsers') +import { userContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { getUserMockInfos, setRequestor } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessGetMatchingMock = vi.spyOn(business, 'getMatchingUsers'); +const businessLogViaSessionMock = vi.spyOn(business, 'logViaSession'); +const businessGetUsersMock = vi.spyOn(business, 'getUsers'); +const businessPatchMock = vi.spyOn(business, 'patchUsers'); describe('test userContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('getMatchingUsers', () => { - it('should return matching users', async () => { - const usersMatching = [] - businessGetMatchingMock.mockResolvedValueOnce(usersMatching) - - const response = await app.inject() - .get(userContract.getMatchingUsers.path) - .query({ letters: faker.person.fullName() }) - .end() - - expect(businessGetMatchingMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(usersMatching) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('auth', () => { - it('should return logged user', async () => { - const user = { - id: faker.string.uuid(), - adminRoleIds: [], - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - type: 'human', - lastName: faker.person.lastName(), - } - setRequestor(user) - businessLogViaSessionMock.mockResolvedValueOnce({ user, adminPerms: 0n }) - - const response = await app.inject() - .get(userContract.auth.path) - .end() - - expect(businessLogViaSessionMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(user) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('getAllUsers', () => { - it('should return all users for admin', async () => { - const user = getUserMockInfos(true) - const users = [] - authUserMock.mockResolvedValueOnce(user) - businessGetUsersMock.mockResolvedValueOnce(users) - - const response = await app.inject() - .get(userContract.getAllUsers.path) - .query({ role: 'admin' }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetUsersMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(users) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(userContract.getAllUsers.path) - .query({ role: 'admin' }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetUsersMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('patchUsers', () => { - const usersPatchData = [{ - id: faker.string.uuid(), - adminRoleIds: [], - }] - const usersReturn = [{ - id: faker.string.uuid(), - adminRoleIds: [], - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - type: 'human', - }] - - it('should patch and return users for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessPatchMock.mockResolvedValueOnce(usersReturn) - const response = await app.inject() - .patch(userContract.patchUsers.path) - .body(usersPatchData) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessPatchMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(usersReturn) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(userContract.patchUsers.path) - .body(usersPatchData) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessPatchMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('getMatchingUsers', () => { + it('should return matching users', async () => { + const usersMatching = []; + businessGetMatchingMock.mockResolvedValueOnce(usersMatching); + + const response = await app + .inject() + .get(userContract.getMatchingUsers.path) + .query({ letters: faker.person.fullName() }) + .end(); + + expect(businessGetMatchingMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(usersMatching); + expect(response.statusCode).toEqual(200); + }); + }); + + describe('auth', () => { + it('should return logged user', async () => { + const user = { + id: faker.string.uuid(), + adminRoleIds: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + type: 'human', + lastName: faker.person.lastName(), + }; + setRequestor(user); + businessLogViaSessionMock.mockResolvedValueOnce({ + user, + adminPerms: 0n, + }); + + const response = await app + .inject() + .get(userContract.auth.path) + .end(); + + expect(businessLogViaSessionMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(user); + expect(response.statusCode).toEqual(200); + }); + }); + + describe('getAllUsers', () => { + it('should return all users for admin', async () => { + const user = getUserMockInfos(true); + const users = []; + authUserMock.mockResolvedValueOnce(user); + businessGetUsersMock.mockResolvedValueOnce(users); + + const response = await app + .inject() + .get(userContract.getAllUsers.path) + .query({ role: 'admin' }) + .end(); + + expect(authUserMock).toHaveBeenCalledTimes(1); + expect(businessGetUsersMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(users); + expect(response.statusCode).toEqual(200); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .get(userContract.getAllUsers.path) + .query({ role: 'admin' }) + .end(); + + expect(authUserMock).toHaveBeenCalledTimes(1); + expect(businessGetUsersMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('patchUsers', () => { + const usersPatchData = [ + { + id: faker.string.uuid(), + adminRoleIds: [], + }, + ]; + const usersReturn = [ + { + id: faker.string.uuid(), + adminRoleIds: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + type: 'human', + }, + ]; + + it('should patch and return users for admin', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessPatchMock.mockResolvedValueOnce(usersReturn); + const response = await app + .inject() + .patch(userContract.patchUsers.path) + .body(usersPatchData) + .end(); + + expect(authUserMock).toHaveBeenCalledTimes(1); + expect(businessPatchMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(usersReturn); + expect(response.statusCode).toEqual(200); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .patch(userContract.patchUsers.path) + .body(usersPatchData) + .end(); + + expect(authUserMock).toHaveBeenCalledTimes(1); + expect(businessPatchMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts index f5369dd1b..4d3c146bf 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts @@ -1,63 +1,73 @@ -import { AdminAuthorized, userContract } from '@cpn-console/shared' +import { AdminAuthorized, userContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import '@old-server/types/index.js'; +import { authUser } from '@old-server/utils/controller.js'; import { - getMatchingUsers, - getUsers, - logViaSession, - patchUsers, -} from './business.js' -import '@old-server/types/index.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors.js' + ErrorResType, + Forbidden403, + Unauthorized401, +} from '@old-server/utils/errors.js'; + +import { + getMatchingUsers, + getUsers, + logViaSession, + patchUsers, +} from './business.js'; export function userRouter() { - return serverInstance.router(userContract, { - getMatchingUsers: async ({ query }) => { - const usersMatching = await getMatchingUsers(query) + return serverInstance.router(userContract, { + getMatchingUsers: async ({ query }) => { + const usersMatching = await getMatchingUsers(query); - return { - status: 200, - body: usersMatching, - } - }, + return { + status: 200, + body: usersMatching, + }; + }, - auth: async ({ request: req }) => { - const user = req.session.user + auth: async ({ request: req }) => { + const user = req.session.user; - if (!user) return new Unauthorized401() + if (!user) return new Unauthorized401(); - const { user: body } = await logViaSession(user) + const { user: body } = await logViaSession(user); - return { - status: 200, - body, - } - }, + return { + status: 200, + body, + }; + }, - getAllUsers: async ({ request: req, query: { relationType, ...query } }) => { - const perms = await authUser(req) + getAllUsers: async ({ + request: req, + query: { relationType, ...query }, + }) => { + const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - const body = await getUsers(query, relationType) - if (body instanceof ErrorResType) return body + const body = await getUsers(query, relationType); + if (body instanceof ErrorResType) return body; - return { - status: 200, - body, - } - }, + return { + status: 200, + body, + }; + }, - patchUsers: async ({ request: req, body }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + patchUsers: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - const users = await patchUsers(body) + const users = await patchUsers(body); - return { - status: 200, - body: users, - } - }, - }) + return { + status: 200, + body: users, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts index 8d491775d..7da8f6fa8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts @@ -1,51 +1,61 @@ -import { createHash } from 'node:crypto' -import type { personalAccessTokenContract } from '@cpn-console/shared' -import { generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' -import type { AdminToken, User } from '@prisma/client' -import prisma from '../../../prisma.js' -import { BadRequest400 } from '@old-server/utils/errors.js' +import type { personalAccessTokenContract } from '@cpn-console/shared'; +import { generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared'; +import { BadRequest400 } from '@old-server/utils/errors.js'; +import type { AdminToken, User } from '@prisma/client'; +import { createHash } from 'node:crypto'; + +import prisma from '../../../prisma.js'; export async function listTokens(userId: User['id']) { - return prisma.personalAccessToken.findMany({ - omit: { hash: true }, - include: { owner: true }, - orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], - where: { userId }, - }) + return prisma.personalAccessToken.findMany({ + omit: { hash: true }, + include: { owner: true }, + orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], + where: { userId }, + }); } -export async function createToken(data: typeof personalAccessTokenContract.createPersonalAccessToken.body._type, userId: User['id']) { - if (data.expirationDate && !isAtLeastTomorrow(new Date(data.expirationDate))) { - return new BadRequest400('Date d\'expiration trop courte') - } - const password = generateRandomPassword(48, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') - const hash = createHash('sha256').update(password).digest('hex') - const token = await prisma.personalAccessToken.create({ - data: { - ...data, - hash, - expirationDate: new Date(data.expirationDate), - userId, - }, - omit: { hash: true }, - include: { owner: true }, - }) - return { - ...token, - password, - } +export async function createToken( + data: typeof personalAccessTokenContract.createPersonalAccessToken.body._type, + userId: User['id'], +) { + if ( + data.expirationDate && + !isAtLeastTomorrow(new Date(data.expirationDate)) + ) { + return new BadRequest400("Date d'expiration trop courte"); + } + const password = generateRandomPassword( + 48, + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-', + ); + const hash = createHash('sha256').update(password).digest('hex'); + const token = await prisma.personalAccessToken.create({ + data: { + ...data, + hash, + expirationDate: new Date(data.expirationDate), + userId, + }, + omit: { hash: true }, + include: { owner: true }, + }); + return { + ...token, + password, + }; } export async function deleteToken(id: AdminToken['id'], userId: User['id']) { - const token = await prisma.personalAccessToken.findUnique({ - where: { - id, - userId, - }, - }) - if (token) { - return prisma.personalAccessToken.delete({ - where: { id }, - }) - } + const token = await prisma.personalAccessToken.findUnique({ + where: { + id, + userId, + }, + }); + if (token) { + return prisma.personalAccessToken.delete({ + where: { id }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts index 5777f5cb5..0d178b3d4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts @@ -1,48 +1,51 @@ -import { personalAccessTokenContract } from '@cpn-console/shared' +import { personalAccessTokenContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import '@old-server/types/index.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; -import '@old-server/types/index.js' -import { createToken, deleteToken, listTokens } from './business.js' -import { serverInstance } from '@old-server/app.js' -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js' +import { createToken, deleteToken, listTokens } from './business.js'; export function personalAccessTokenRouter() { - return serverInstance.router(personalAccessTokenContract, { - listPersonalAccessTokens: async ({ request: req }) => { - const perms = await authUser(req) - - if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - const body = await listTokens(perms.user.id) - - return { - status: 200, - body, - } - }, - - createPersonalAccessToken: async ({ request: req, body: data }) => { - const perms = await authUser(req) - - if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - const body = await createToken(data, perms.user.id) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - deletePersonalAccessToken: async ({ request: req, params }) => { - const perms = await authUser(req) - - if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - await deleteToken(params.tokenId, perms.user.id) - - return { - status: 204, - body: null, - } - }, - }) + return serverInstance.router(personalAccessTokenContract, { + listPersonalAccessTokens: async ({ request: req }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + const body = await listTokens(perms.user.id); + + return { + status: 200, + body, + }; + }, + + createPersonalAccessToken: async ({ request: req, body: data }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + const body = await createToken(data, perms.user.id); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + deletePersonalAccessToken: async ({ request: req, params }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + await deleteToken(params.tokenId, perms.user.id); + + return { + status: 204, + body: null, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts index b126fce18..2298b5de9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts @@ -1,133 +1,159 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Cluster, Zone } from '@prisma/client' -import prisma from '../../__mocks__/prisma.js' -import { BadRequest400 } from '../../utils/errors.ts' -import { hook } from '../../__mocks__/utils/hook-wrapper.ts' -import { createZone, deleteZone, listZones, updateZone } from './business.ts' -import * as queries from './queries.js' +import { faker } from '@faker-js/faker'; +import type { Cluster, Zone } from '@prisma/client'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -const userId = faker.string.uuid() -const reqId = faker.string.uuid() -const linkZoneToClustersMock = vi.spyOn(queries, 'linkZoneToClusters') +import prisma from '../../__mocks__/prisma.js'; +import { hook } from '../../__mocks__/utils/hook-wrapper.ts'; +import { BadRequest400 } from '../../utils/errors.ts'; +import { createZone, deleteZone, listZones, updateZone } from './business.ts'; +import * as queries from './queries.js'; + +const userId = faker.string.uuid(); +const reqId = faker.string.uuid(); +const linkZoneToClustersMock = vi.spyOn(queries, 'linkZoneToClusters'); vi.mock('../../utils/hook-wrapper.ts', async () => ({ - hook, -})) + hook, +})); describe('test zone business', () => { - const zones: Zone[] = [{ - id: faker.string.uuid(), - label: faker.company.name(), - argocdUrl: faker.internet.url(), - createdAt: new Date(), - updatedAt: new Date(), - description: faker.lorem.lines(1), - slug: faker.string.alphanumeric(5), - }, { - id: faker.string.uuid(), - label: faker.company.name(), - argocdUrl: faker.internet.url(), - createdAt: new Date(), - updatedAt: new Date(), - description: faker.lorem.lines(1), - slug: faker.string.alphanumeric(6), - }] + const zones: Zone[] = [ + { + id: faker.string.uuid(), + label: faker.company.name(), + argocdUrl: faker.internet.url(), + createdAt: new Date(), + updatedAt: new Date(), + description: faker.lorem.lines(1), + slug: faker.string.alphanumeric(5), + }, + { + id: faker.string.uuid(), + label: faker.company.name(), + argocdUrl: faker.internet.url(), + createdAt: new Date(), + updatedAt: new Date(), + description: faker.lorem.lines(1), + slug: faker.string.alphanumeric(6), + }, + ]; - const clusters: Pick[] = [ - { id: faker.string.uuid() }, - { id: faker.string.uuid() }, - ] + const clusters: Pick[] = [ + { id: faker.string.uuid() }, + { id: faker.string.uuid() }, + ]; - beforeEach(() => { - vi.resetAllMocks() - }) - describe('listZones', () => { - it('should return zones', async () => { - prisma.zone.findMany.mockResolvedValueOnce(zones) + beforeEach(() => { + vi.resetAllMocks(); + }); + describe('listZones', () => { + it('should return zones', async () => { + prisma.zone.findMany.mockResolvedValueOnce(zones); - const response = await listZones() - expect(response).toEqual(zones) - }) - }) - describe('createZone', () => { - it('should create zone without description and clusterIds', async () => { - const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl } + const response = await listZones(); + expect(response).toEqual(zones); + }); + }); + describe('createZone', () => { + it('should create zone without description and clusterIds', async () => { + const newZone = { + label: zones[0].label, + slug: zones[0].slug, + argocdUrl: zones[0].argocdUrl, + }; - hook.zone.upsert.mockResolvedValue({}) - prisma.zone.create.mockResolvedValueOnce(zones[0]) - const response = await createZone(newZone, userId, reqId) + hook.zone.upsert.mockResolvedValue({}); + prisma.zone.create.mockResolvedValueOnce(zones[0]); + const response = await createZone(newZone, userId, reqId); - expect(response).toEqual(zones[0]) - expect(prisma.zone.create).toHaveBeenCalledWith({ - data: { - slug: newZone.slug, - label: newZone.label, - argocdUrl: newZone.argocdUrl, - description: undefined, - }, - }) - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0) - }) - it('should create zone with description and clusterIds', async () => { - const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl, clusterIds: clusters.map(({ id }) => id), description: faker.lorem.lines(2) } + expect(response).toEqual(zones[0]); + expect(prisma.zone.create).toHaveBeenCalledWith({ + data: { + slug: newZone.slug, + label: newZone.label, + argocdUrl: newZone.argocdUrl, + description: undefined, + }, + }); + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0); + }); + it('should create zone with description and clusterIds', async () => { + const newZone = { + label: zones[0].label, + slug: zones[0].slug, + argocdUrl: zones[0].argocdUrl, + clusterIds: clusters.map(({ id }) => id), + description: faker.lorem.lines(2), + }; - hook.zone.upsert.mockResolvedValue({}) - prisma.zone.create.mockResolvedValueOnce(zones[0]) - const response = await createZone(newZone, userId, reqId) + hook.zone.upsert.mockResolvedValue({}); + prisma.zone.create.mockResolvedValueOnce(zones[0]); + const response = await createZone(newZone, userId, reqId); - expect(response).toEqual(zones[0]) - expect(prisma.zone.create).toHaveBeenCalledWith({ - data: { - description: newZone.description, - label: newZone.label, - argocdUrl: newZone.argocdUrl, - slug: newZone.slug, - }, - }) - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(1) - }) - it('should not create zone, conflict label', async () => { - const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl } + expect(response).toEqual(zones[0]); + expect(prisma.zone.create).toHaveBeenCalledWith({ + data: { + description: newZone.description, + label: newZone.label, + argocdUrl: newZone.argocdUrl, + slug: newZone.slug, + }, + }); + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(1); + }); + it('should not create zone, conflict label', async () => { + const newZone = { + label: zones[0].label, + slug: zones[0].slug, + argocdUrl: zones[0].argocdUrl, + }; - prisma.zone.findUnique.mockResolvedValueOnce(zones[0]) - prisma.zone.create.mockResolvedValueOnce(zones[0]) - const response = await createZone(newZone, userId, reqId) + prisma.zone.findUnique.mockResolvedValueOnce(zones[0]); + prisma.zone.create.mockResolvedValueOnce(zones[0]); + const response = await createZone(newZone, userId, reqId); - expect(response).instanceOf(BadRequest400) - expect(prisma.zone.create).toHaveBeenCalledTimes(0) - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0) - }) - }) - describe('updateZone', () => { - it('should filter keys and update zone', async () => { - prisma.zone.update.mockResolvedValueOnce(zones[0]) - hook.zone.upsert.mockResolvedValue({}) - await updateZone(zones[0].id, { - description: '', - label: zones[0].label, - argocdUrl: zones[0].argocdUrl, - extraKey: 1, - }, userId, reqId) - expect(prisma.zone.update).toHaveBeenCalledWith({ where: { id: zones[0].id }, data: { - description: '', - label: zones[0].label, - argocdUrl: zones[0].argocdUrl, - } }) - }) - }) - describe('deleteZone', () => { - it('should not delete zone, cluster attached', async () => { - prisma.cluster.findFirst.mockResolvedValueOnce(clusters[0]) - const response = await deleteZone(zones[0].id, userId, reqId) - expect(response).instanceOf(BadRequest400) - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) - }) - it('should delete zone', async () => { - prisma.cluster.findFirst.mockResolvedValueOnce(undefined) - hook.zone.delete.mockResolvedValue({}) - const response = await deleteZone(zones[0].id, userId, reqId) - expect(response).toEqual(null) - expect(prisma.zone.delete).toHaveBeenCalledTimes(1) - }) - }) -}) + expect(response).instanceOf(BadRequest400); + expect(prisma.zone.create).toHaveBeenCalledTimes(0); + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0); + }); + }); + describe('updateZone', () => { + it('should filter keys and update zone', async () => { + prisma.zone.update.mockResolvedValueOnce(zones[0]); + hook.zone.upsert.mockResolvedValue({}); + await updateZone( + zones[0].id, + { + description: '', + label: zones[0].label, + argocdUrl: zones[0].argocdUrl, + extraKey: 1, + }, + userId, + reqId, + ); + expect(prisma.zone.update).toHaveBeenCalledWith({ + where: { id: zones[0].id }, + data: { + description: '', + label: zones[0].label, + argocdUrl: zones[0].argocdUrl, + }, + }); + }); + }); + describe('deleteZone', () => { + it('should not delete zone, cluster attached', async () => { + prisma.cluster.findFirst.mockResolvedValueOnce(clusters[0]); + const response = await deleteZone(zones[0].id, userId, reqId); + expect(response).instanceOf(BadRequest400); + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0); + }); + it('should delete zone', async () => { + prisma.cluster.findFirst.mockResolvedValueOnce(undefined); + hook.zone.delete.mockResolvedValue({}); + const response = await deleteZone(zones[0].id, userId, reqId); + expect(response).toEqual(null); + expect(prisma.zone.delete).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts index c5a0b7d29..db98b62c0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts @@ -1,78 +1,119 @@ -import type { User, Zone } from '@cpn-console/shared' -import { addLogs } from '../queries-index.js' -import { linkZoneToClusters } from './queries.js' -import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors.js' -import prisma from '@old-server/prisma.js' -import { hook } from '@old-server/utils/hook-wrapper.js' +import type { User, Zone } from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; +import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors.js'; +import { hook } from '@old-server/utils/hook-wrapper.js'; -export const listZones = prisma.zone.findMany +import { addLogs } from '../queries-index.js'; +import { linkZoneToClusters } from './queries.js'; + +export const listZones = prisma.zone.findMany; export async function createZone( - data: { slug: string, label: string, argocdUrl: string, description?: string | null, clusterIds?: string[] }, - userId: User['id'], - requestId: string, + data: { + slug: string; + label: string; + argocdUrl: string; + description?: string | null; + clusterIds?: string[]; + }, + userId: User['id'], + requestId: string, ) { - const { slug, label, argocdUrl, description, clusterIds } = data + const { slug, label, argocdUrl, description, clusterIds } = data; - const existingZone = await prisma.zone.findUnique({ - where: { slug }, - }) + const existingZone = await prisma.zone.findUnique({ + where: { slug }, + }); - if (existingZone) return new BadRequest400(`Une zone portant le nom ${slug} existe déjà.`) - const zone = await prisma.zone.create({ - data: { - slug, - label, - argocdUrl, - description, - }, - }) - if (clusterIds) { - await linkZoneToClusters(zone.id, clusterIds) - } - const hookReply = await hook.zone.upsert(zone.id) - await addLogs({ action: 'Create zone', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services lors de la création de la zone') - } - return zone + if (existingZone) + return new BadRequest400( + `Une zone portant le nom ${slug} existe déjà.`, + ); + const zone = await prisma.zone.create({ + data: { + slug, + label, + argocdUrl, + description, + }, + }); + if (clusterIds) { + await linkZoneToClusters(zone.id, clusterIds); + } + const hookReply = await hook.zone.upsert(zone.id); + await addLogs({ + action: 'Create zone', + data: hookReply, + userId, + requestId, + }); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services lors de la création de la zone', + ); + } + return zone; } export async function updateZone( - zoneId: Zone['id'], - data: Pick, - userId: User['id'], - requestId: string, + zoneId: Zone['id'], + data: Pick, + userId: User['id'], + requestId: string, ) { - const { label, argocdUrl, description } = data + const { label, argocdUrl, description } = data; - const updatedZone = await prisma.zone.update({ - where: { - id: zoneId, - }, - data: { - label, - argocdUrl, - description, - }, - }) - const hookReply = await hook.zone.upsert(updatedZone.id) - await addLogs({ action: 'Update zone', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services lors de la mise à jour de la zone') - } - return updatedZone + const updatedZone = await prisma.zone.update({ + where: { + id: zoneId, + }, + data: { + label, + argocdUrl, + description, + }, + }); + const hookReply = await hook.zone.upsert(updatedZone.id); + await addLogs({ + action: 'Update zone', + data: hookReply, + userId, + requestId, + }); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services lors de la mise à jour de la zone', + ); + } + return updatedZone; } -export async function deleteZone(zoneId: Zone['id'], userId: User['id'], requestId: string) { - const attachedCluster = await prisma.cluster.findFirst({ where: { zoneId }, select: { id: true } }) - if (attachedCluster) return new BadRequest400('Vous ne pouvez supprimer cette zone, car des clusters y sont associés.') +export async function deleteZone( + zoneId: Zone['id'], + userId: User['id'], + requestId: string, +) { + const attachedCluster = await prisma.cluster.findFirst({ + where: { zoneId }, + select: { id: true }, + }); + if (attachedCluster) + return new BadRequest400( + 'Vous ne pouvez supprimer cette zone, car des clusters y sont associés.', + ); - const hookReply = await hook.zone.delete(zoneId) - await addLogs({ action: 'Delete zone', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services lors de la suppression de la zone') - } - await prisma.zone.delete({ where: { id: zoneId } }) - return null + const hookReply = await hook.zone.delete(zoneId); + await addLogs({ + action: 'Delete zone', + data: hookReply, + userId, + requestId, + }); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services lors de la suppression de la zone', + ); + } + await prisma.zone.delete({ where: { id: zoneId } }); + return null; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts index 255287000..65ae14df1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts @@ -1,21 +1,24 @@ -import type { Cluster, Zone } from '@prisma/client' -import prisma from '@old-server/prisma.js' +import prisma from '@old-server/prisma.js'; +import type { Cluster, Zone } from '@prisma/client'; export function getZoneByIdOrThrow(id: Zone['id']) { - return prisma.zone.findUniqueOrThrow({ - where: { id }, - }) + return prisma.zone.findUniqueOrThrow({ + where: { id }, + }); } -export function linkZoneToClusters(zoneId: Zone['id'], clusterIds: Cluster['id'][]) { - return prisma.zone.update({ - where: { - id: zoneId, - }, - data: { - clusters: { - connect: clusterIds.map(clusterId => ({ id: clusterId })), - }, - }, - }) +export function linkZoneToClusters( + zoneId: Zone['id'], + clusterIds: Cluster['id'][], +) { + return prisma.zone.update({ + where: { + id: zoneId, + }, + data: { + clusters: { + connect: clusterIds.map((clusterId) => ({ id: clusterId })), + }, + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts index 7cec26cd3..2d992ee64 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts @@ -1,162 +1,208 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Zone } from '@cpn-console/shared' -import { zoneContract } from '@cpn-console/shared' -import app from '../../app.js' -import * as utilsController from '../../utils/controller.js' -import { getUserMockInfos } from '../../utils/mocks.js' -import { BadRequest400 } from '../../utils/errors.js' -import * as business from './business.js' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks.js')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMock = vi.spyOn(business, 'listZones') -const businessCreateMock = vi.spyOn(business, 'createZone') -const businessUpdateMock = vi.spyOn(business, 'updateZone') -const businessDeleteMock = vi.spyOn(business, 'deleteZone') +import type { Zone } from '@cpn-console/shared'; +import { zoneContract } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../app.js'; +import * as utilsController from '../../utils/controller.js'; +import { BadRequest400 } from '../../utils/errors.js'; +import { getUserMockInfos } from '../../utils/mocks.js'; +import * as business from './business.js'; + +vi.mock( + 'fastify-keycloak-adapter', + (await import('../../utils/mocks.js')).mockSessionPlugin, +); +const authUserMock = vi.spyOn(utilsController, 'authUser'); +const businessListMock = vi.spyOn(business, 'listZones'); +const businessCreateMock = vi.spyOn(business, 'createZone'); +const businessUpdateMock = vi.spyOn(business, 'updateZone'); +const businessDeleteMock = vi.spyOn(business, 'deleteZone'); describe('test zoneContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listZones', () => { - it('should return list of zones', async () => { - const zones = [] - businessListMock.mockResolvedValueOnce(zones) - - const response = await app.inject() - .get(zoneContract.listZones.path) - .end() - - expect(businessListMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(zones) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('createZone', () => { - const zone = { id: faker.string.uuid(), label: faker.string.alpha({ length: 5 }), argocdUrl: faker.internet.url(), slug: faker.string.alpha({ length: 5, casing: 'lower' }), description: '' } - - it('should create and return zone for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(zone) - const response = await app.inject() - .post(zoneContract.createZone.path) - .body(zone) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(zone) - expect(response.statusCode).toEqual(201) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(zoneContract.createZone.path) - .body(zone) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(zoneContract.createZone.path) - .body(zone) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('updateZone', () => { - const zoneId = faker.string.uuid() - const zone: Omit = { label: faker.string.alpha({ length: 5 }), slug: faker.string.alpha({ length: 5, casing: 'lower' }), argocdUrl: faker.internet.url(), description: '' } - - it('should update and return zone for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: zoneId, ...zone }) - const response = await app.inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ id: zoneId, ...zone }) - expect(response.statusCode).toEqual(200) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('deleteZone', () => { - it('should delete zone for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(1) - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('listZones', () => { + it('should return list of zones', async () => { + const zones = []; + businessListMock.mockResolvedValueOnce(zones); + + const response = await app + .inject() + .get(zoneContract.listZones.path) + .end(); + + expect(businessListMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(zones); + expect(response.statusCode).toEqual(200); + }); + }); + + describe('createZone', () => { + const zone = { + id: faker.string.uuid(), + label: faker.string.alpha({ length: 5 }), + argocdUrl: faker.internet.url(), + slug: faker.string.alpha({ length: 5, casing: 'lower' }), + description: '', + }; + + it('should create and return zone for admin', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce(zone); + const response = await app + .inject() + .post(zoneContract.createZone.path) + .body(zone) + .end(); + + expect(businessCreateMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual(zone); + expect(response.statusCode).toEqual(201); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessCreateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .post(zoneContract.createZone.path) + .body(zone) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .post(zoneContract.createZone.path) + .body(zone) + .end(); + + expect(businessCreateMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('updateZone', () => { + const zoneId = faker.string.uuid(); + const zone: Omit = { + label: faker.string.alpha({ length: 5 }), + slug: faker.string.alpha({ length: 5, casing: 'lower' }), + argocdUrl: faker.internet.url(), + description: '', + }; + + it('should update and return zone for admin', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce({ id: zoneId, ...zone }); + const response = await app + .inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledTimes(1); + expect(response.json()).toEqual({ id: zoneId, ...zone }); + expect(response.statusCode).toEqual(200); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessUpdateMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end(); + + expect(businessUpdateMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('deleteZone', () => { + it('should delete zone for admin', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce(null); + const response = await app + .inject() + .delete( + zoneContract.deleteZone.path.replace( + ':zoneId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessDeleteMock).toHaveBeenCalledTimes(1); + expect(response.body).toBeFalsy(); + expect(response.statusCode).toEqual(204); + }); + it('should pass business error', async () => { + const user = getUserMockInfos(true); + authUserMock.mockResolvedValueOnce(user); + + businessDeleteMock.mockResolvedValueOnce( + new BadRequest400('une erreur'), + ); + const response = await app + .inject() + .delete( + zoneContract.deleteZone.path.replace( + ':zoneId', + faker.string.uuid(), + ), + ) + .end(); + + expect(response.statusCode).toEqual(400); + }); + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false); + authUserMock.mockResolvedValueOnce(user); + + const response = await app + .inject() + .delete( + zoneContract.deleteZone.path.replace( + ':zoneId', + faker.string.uuid(), + ), + ) + .end(); + + expect(businessDeleteMock).toHaveBeenCalledTimes(0); + expect(response.statusCode).toEqual(403); + }); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts index 4a583a2f5..f0bf94824 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts @@ -1,64 +1,80 @@ -import { AdminAuthorized, zoneContract } from '@cpn-console/shared' -import { createZone, deleteZone, listZones, updateZone } from './business.js' -import { serverInstance } from '@old-server/app.js' +import { AdminAuthorized, zoneContract } from '@cpn-console/shared'; +import { serverInstance } from '@old-server/app.js'; +import { authUser } from '@old-server/utils/controller.js'; +import { + ErrorResType, + Forbidden403, + Unauthorized401, +} from '@old-server/utils/errors.js'; -import { authUser } from '@old-server/utils/controller.js' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors.js' +import { createZone, deleteZone, listZones, updateZone } from './business.js'; export function zoneRouter() { - return serverInstance.router(zoneContract, { - listZones: async () => { - const zones = await listZones() + return serverInstance.router(zoneContract, { + listZones: async () => { + const zones = await listZones(); - return { - status: 200, - body: zones, - } - }, + return { + status: 200, + body: zones, + }; + }, - createZone: async ({ request: req, body: data }) => { - const { user, adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user) return new Unauthorized401('Require to be requested from user not api key') + createZone: async ({ request: req, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); - const body = await createZone(data, user.id, req.id) - if (body instanceof ErrorResType) return body + const body = await createZone(data, user.id, req.id); + if (body instanceof ErrorResType) return body; - return { - status: 201, - body, - } - }, + return { + status: 201, + body, + }; + }, - updateZone: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user) return new Unauthorized401('Require to be requested from user not api key') + updateZone: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); - const zoneId = params.zoneId + const zoneId = params.zoneId; - const body = await updateZone(zoneId, data, user.id, req.id) - if (body instanceof ErrorResType) return body + const body = await updateZone(zoneId, data, user.id, req.id); + if (body instanceof ErrorResType) return body; - return { - status: 200, - body, - } - }, + return { + status: 200, + body, + }; + }, - deleteZone: async ({ request: req, params }) => { - const { user, adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user) return new Unauthorized401('Require to be requested from user not api key') - const zoneId = params.zoneId + deleteZone: async ({ request: req, params }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + const zoneId = params.zoneId; - const body = await deleteZone(zoneId, user.id, req.id) - if (body instanceof ErrorResType) return body + const body = await deleteZone(zoneId, user.id, req.id); + if (body instanceof ErrorResType) return body; - return { - status: 204, - body, - } - }, - }) + return { + status: 204, + body, + }; + }, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts index 8620cc2fa..0d3113084 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts @@ -1,57 +1,61 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { exitGracefully, handleExit } from './server.js' -import { closeConnections } from './connect.js' -import { logger } from './app.js' +import { beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks.js')).mockSessionPlugin) -vi.mock('./init/db/index.js', () => ({ initDb: vi.fn() })) -vi.mock('./connect.js') +import { logger } from './app.js'; +import { closeConnections } from './connect.js'; +import { exitGracefully, handleExit } from './server.js'; -process.exit = vi.fn() +vi.mock( + 'fastify-keycloak-adapter', + (await import('./utils/mocks.js')).mockSessionPlugin, +); +vi.mock('./init/db/index.js', () => ({ initDb: vi.fn() })); +vi.mock('./connect.js'); + +process.exit = vi.fn(); vi.mock('./prepare-app.js', () => { - const app = { - listen: vi.fn(), - close: vi.fn(async () => {}), - } - return { - getPreparedApp: () => Promise.resolve(app), - } -}) -vi.spyOn(logger, 'info') -vi.spyOn(logger, 'warn') -vi.spyOn(logger, 'error') -vi.spyOn(logger, 'fatal') -vi.spyOn(logger, 'debug') + const app = { + listen: vi.fn(), + close: vi.fn(async () => {}), + }; + return { + getPreparedApp: () => Promise.resolve(app), + }; +}); +vi.spyOn(logger, 'info'); +vi.spyOn(logger, 'warn'); +vi.spyOn(logger, 'error'); +vi.spyOn(logger, 'fatal'); +vi.spyOn(logger, 'debug'); describe('server', () => { - beforeEach(() => { - vi.clearAllMocks() - }) + beforeEach(() => { + vi.clearAllMocks(); + }); - it('should call closeConnections without parameter', async () => { - await exitGracefully() + it('should call closeConnections without parameter', async () => { + await exitGracefully(); - expect(closeConnections).toHaveBeenCalledTimes(1) - expect(closeConnections.mock.calls[0]).toHaveLength(0) - expect(logger.error).toHaveBeenCalledTimes(0) - }) + expect(closeConnections).toHaveBeenCalledTimes(1); + expect(closeConnections.mock.calls[0]).toHaveLength(0); + expect(logger.error).toHaveBeenCalledTimes(0); + }); - it('should log an error', async () => { - await exitGracefully(new Error('error')) + it('should log an error', async () => { + await exitGracefully(new Error('error')); - expect(closeConnections).toHaveBeenCalledTimes(1) - expect(closeConnections.mock.calls[0]).toHaveLength(0) - expect(logger.fatal).toHaveBeenCalledTimes(1) - expect(logger.fatal.mock.calls[0][0]).toBeInstanceOf(Error) - expect(logger.info).toHaveBeenCalledTimes(2) - }) + expect(closeConnections).toHaveBeenCalledTimes(1); + expect(closeConnections.mock.calls[0]).toHaveLength(0); + expect(logger.fatal).toHaveBeenCalledTimes(1); + expect(logger.fatal.mock.calls[0][0]).toBeInstanceOf(Error); + expect(logger.info).toHaveBeenCalledTimes(2); + }); - it('should call process.on 4 times', () => { - const processOn = vi.spyOn(process, 'on') + it('should call process.on 4 times', () => { + const processOn = vi.spyOn(process, 'on'); - handleExit() + handleExit(); - expect(processOn).toHaveBeenCalledTimes(5) - }) -}) + expect(processOn).toHaveBeenCalledTimes(5); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts index 7d1497a8e..4f92206dc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts @@ -1,44 +1,155 @@ -import { getPreparedApp } from './prepare-app.js' -import { closeConnections } from './connect.js' -import { isCI, isDev, isDevSetup, isProd, isTest, port } from './utils/env.js' -import { logger } from './app.js' - -const app = await getPreparedApp() - -try { - await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }) -} catch (error) { - logger.error(error) - process.exit(1) -} +import { apiPrefix, getContract } from '@cpn-console/shared'; +import fastifyCookie from '@fastify/cookie'; +import helmet from '@fastify/helmet'; +import fastifySession from '@fastify/session'; +import fastifySwagger from '@fastify/swagger'; +import fastifySwaggerUi from '@fastify/swagger-ui'; +import { Injectable } from '@nestjs/common'; +import { initServer } from '@ts-rest/fastify'; +import { generateOpenApi } from '@ts-rest/open-api'; +import type { FastifyRequest } from 'fastify'; +import fastify from 'fastify'; +import keycloak from 'fastify-keycloak-adapter'; -logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) - -export async function exitGracefully(error?: Error) { - if (error instanceof Error) { - logger.fatal(error) - } - await app.close() - logger.info('Closing connections...') - await closeConnections() - logger.info('Exiting...') - process.exit(error instanceof Error ? 1 : 0) -} +import { AppService } from './app'; +import { ConnectionService } from './connect'; +import { getPreparedApp } from './prepare-app'; +import { apiRouter } from './resources/index.js'; +import { + isCI, + isDev, + isDevSetup, + isInt, + isProd, + isTest, + port, +} from './utils/env.js'; +import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify.js'; +import { keycloakConf, sessionConf } from './utils/keycloak.js'; +import type { CustomLogger } from './utils/logger.js'; +import { log } from './utils/logger.js'; -function logExitCode(code: number) { - logger.warn(`received signal: ${code}`) -} +@Injectable() +export class ServerService { + constructor( + private readonly connectionService: ConnectionService, + private readonly appService: AppService, + ) {} -function logUnhandledRejection(reason: unknown, promise: Promise) { - logger.error({ message: 'Unhandled Rejection', promise, reason }) -} + app: any; + serverInstance: ReturnType = initServer(); + logger: any; -export function handleExit() { - process.on('exit', logExitCode) - process.on('SIGINT', exitGracefully) - process.on('SIGTERM', exitGracefully) - process.on('uncaughtException', exitGracefully) - process.on('unhandledRejection', logUnhandledRejection) -} + handleExit() { + process.on('exit', this.logExitCode); + process.on('SIGINT', this.exitGracefully); + process.on('SIGTERM', this.exitGracefully); + process.on('uncaughtException', this.exitGracefully); + process.on('unhandledRejection', this.logUnhandledRejection); + } + + logExitCode(code: number) { + this.appService.logger.warn(`received signal: ${code}`); + } + + logUnhandledRejection(reason: unknown, promise: Promise) { + this.appService.logger.error({ + message: 'Unhandled Rejection', + promise, + reason, + }); + } + + async exitGracefully(error?: Error) { + if (error instanceof Error) { + this.appService.logger.fatal(error); + } + await this.app.close(); + this.appService.logger.info('Closing connections...'); + await this.connectionService.closeConnections(); + this.appService.logger.info('Exiting...'); + process.exit(error instanceof Error ? 1 : 0); + } -handleExit() + async getApp(): Promise { + const app = await getPreparedApp(); + + try { + await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }); + } catch (error) { + this.appService.logger.error(error); + process.exit(1); + } + + this.appService.logger.debug({ + isDev, + isTest, + isCI, + isDevSetup, + isProd, + }); + this.handleExit(); + } + + async createApp() { + const openApiDocument = generateOpenApi( + await getContract(), + swaggerConf, + { + setOperationId: true, + }, + ); + + const app = fastify(fastifyConf) + .register(helmet, () => ({ + contentSecurityPolicy: !(isInt || isDev || isTest), + })) + .register(fastifyCookie) + .register(fastifySession, sessionConf) + // @ts-ignore + .register(keycloak, keycloakConf) + .register(fastifySwagger, { + transformObject: () => openApiDocument, + }) + .register(fastifySwaggerUi, swaggerUiConf) + .register(apiRouter()) + .addHook('onRoute', (opts) => { + if (opts.path === `${apiPrefix}/healthz`) { + opts.logLevel = 'silent'; + } + }) + .setErrorHandler((error: Error, req: FastifyRequest, reply) => { + const statusCode = 500; + // @ts-ignore vérifier l'objet + const message = error.description || error.message; + reply.status(statusCode).send({ + status: statusCode, + error: message, + stack: error.stack, + }); + log('info', { reqId: req.id, error }); + }) + .addHook('onResponse', (req, res) => { + if (res.statusCode < 400) { + req.log.info({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } else if (res.statusCode < 500) { + req.log.warn({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } else { + req.log.error({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } + }); + + await app.ready(); + + this.logger = app.log as CustomLogger; + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts index 282b1c43d..e8f91a65b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts @@ -1,41 +1,46 @@ -import { type SharedSafeParseReturnType, parseZodError } from '@cpn-console/shared' -import { BadRequest400 } from './errors.js' +import { + type SharedSafeParseReturnType, + parseZodError, +} from '@cpn-console/shared'; -export type Success = Result -export type Failure = Result +import { BadRequest400 } from './errors.js'; + +export type Success = Result; +export type Failure = Result; export class Result { - protected constructor( - readonly success: boolean, - readonly value: T | string, - ) {} - - static succeed(value: T): Success { - return new Result(true, value) as Success - } - - static fail(message: string): Failure { - return new Result(false, message) as Failure - } - - get isSuccess(): boolean { - return this.success - } - - get isError(): boolean { - return !this.success - } - - get data(): T { - if (this.success) return this.value as T - throw new Error('Cannot get data from a Failure') - } - - get error(): string { - if (!this.success) return this.value as string - throw new Error('Cannot get error from a Success') - } + protected constructor( + readonly success: boolean, + readonly value: T | string, + ) {} + + static succeed(value: T): Success { + return new Result(true, value) as Success; + } + + static fail(message: string): Failure { + return new Result(false, message) as Failure; + } + + get isSuccess(): boolean { + return this.success; + } + + get isError(): boolean { + return !this.success; + } + + get data(): T { + if (this.success) return this.value as T; + throw new Error('Cannot get data from a Failure'); + } + + get error(): string { + if (!this.success) return this.value as string; + throw new Error('Cannot get error from a Success'); + } } export function validateSchema(schemaValidation: SharedSafeParseReturnType) { - if (!schemaValidation.success) return new BadRequest400(parseZodError(schemaValidation.error)) + if (!schemaValidation.success) + return new BadRequest400(parseZodError(schemaValidation.error)); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts index ba3eb8425..438067a6e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts @@ -1,169 +1,236 @@ -import type { Cluster, Prisma, Project, ProjectMembers, ProjectRole } from '@prisma/client' -import type { XOR } from '@cpn-console/shared' -import { PROJECT_PERMS as PP, PROJECT_PERMS, projectIsLockedInfo, tokenHeaderName } from '@cpn-console/shared' -import type { FastifyRequest } from 'fastify' -import { Unauthorized401 } from './errors.js' -import { uuid } from './queries-tools.js' -import type { UserDetails } from '@old-server/types/index.js' -import prisma from '@old-server/prisma.js' -import { logViaSession, logViaToken } from '@old-server/resources/user/business.js' - -export type RequireOnlyOne = - Pick> - & { - [K in Keys]-?: - Required> - & Partial, undefined>> - }[Keys] - -type ErrorMessagePredicate = () => string | undefined +import type { XOR } from '@cpn-console/shared'; +import { + PROJECT_PERMS as PP, + PROJECT_PERMS, + projectIsLockedInfo, + tokenHeaderName, +} from '@cpn-console/shared'; +import prisma from '@old-server/prisma.js'; +import { + logViaSession, + logViaToken, +} from '@old-server/resources/user/business.js'; +import type { UserDetails } from '@old-server/types/index.js'; +import type { + Cluster, + Prisma, + Project, + ProjectMembers, + ProjectRole, +} from '@prisma/client'; +import type { FastifyRequest } from 'fastify'; + +import { Unauthorized401 } from './errors.js'; +import { uuid } from './queries-tools.js'; + +export type RequireOnlyOne = Pick< + T, + Exclude +> & + { + [K in Keys]-?: Required> & + Partial, undefined>>; + }[Keys]; + +type ErrorMessagePredicate = () => string | undefined; export function getErrorMessage(...fns: ErrorMessagePredicate[]) { - for (const f of fns) { - const error = f() - if (error) { - return error + for (const f of fns) { + const error = f(); + if (error) { + return error; + } } - } } /** * Renvoie une erreur si le projet est verrouillé */ export function checkProjectLocked(project: { locked: boolean }): string { - return project.locked - ? projectIsLockedInfo - : '' + return project.locked ? projectIsLockedInfo : ''; } export function checkLocked(project: { locked: Project['locked'] }): string { - return checkProjectLocked(project) + return checkProjectLocked(project); } -export function checkClusterUnavailable(clusterId: Cluster['id'], authorizedClusterIds: Cluster['id'][]): string { - return authorizedClusterIds.includes(clusterId) - ? '' - : 'Ce cluster n\'est pas disponible pour cette combinaison projet et stage' +export function checkClusterUnavailable( + clusterId: Cluster['id'], + authorizedClusterIds: Cluster['id'][], +): string { + return authorizedClusterIds.includes(clusterId) + ? '' + : "Ce cluster n'est pas disponible pour cette combinaison projet et stage"; } -export const splitStringsFilterArray = >(toMatch: T, inputs: string): T => inputs.split(',').filter(i => toMatch.includes(i)) as unknown as T +export const splitStringsFilterArray = >( + toMatch: T, + inputs: string, +): T => inputs.split(',').filter((i) => toMatch.includes(i)) as unknown as T; -type StringArray = string[] +type StringArray = string[]; interface WhereBuilderParams { - enumValues: T - eqValue: T[number] | undefined - inValues: string | undefined - notInValues: string | undefined + enumValues: T; + eqValue: T[number] | undefined; + inValues: string | undefined; + notInValues: string | undefined; } -export function whereBuilder({ enumValues, eqValue, inValues, notInValues }: WhereBuilderParams) { - if (eqValue) { - return eqValue - } else if (inValues) { - return { in: splitStringsFilterArray(enumValues, inValues) } - } else if (notInValues) { - return { notIn: splitStringsFilterArray(enumValues, notInValues) } - } +export function whereBuilder({ + enumValues, + eqValue, + inValues, + notInValues, +}: WhereBuilderParams) { + if (eqValue) { + return eqValue; + } else if (inValues) { + return { in: splitStringsFilterArray(enumValues, inValues) }; + } else if (notInValues) { + return { notIn: splitStringsFilterArray(enumValues, notInValues) }; + } } -type ProjectMinimalPerms = Pick & { roles: ProjectRole[], members: ProjectMembers[] } -export interface UserProfile { user?: UserDetails, adminPermissions: bigint, tokenId?: string } -export interface ProjectPermState { projectPermissions?: bigint, projectId: Project['id'], projectLocked: boolean, projectStatus: Project['status'], projectOwnerId: Project['ownerId'] } -export type UserProjectProfile = UserProfile & ProjectPermState +type ProjectMinimalPerms = Pick< + Project, + 'everyonePerms' | 'ownerId' | 'id' | 'locked' | 'status' +> & { roles: ProjectRole[]; members: ProjectMembers[] }; +export interface UserProfile { + user?: UserDetails; + adminPermissions: bigint; + tokenId?: string; +} +export interface ProjectPermState { + projectPermissions?: bigint; + projectId: Project['id']; + projectLocked: boolean; + projectStatus: Project['status']; + projectOwnerId: Project['ownerId']; +} +export type UserProjectProfile = UserProfile & ProjectPermState; type ProjectUniqueFinder = XOR< - { slug: string }, - XOR<{ environmentId: string }, XOR<{ repositoryId: string }, { id: string }>> -> - -const projectPermsSelect = { roles: true, members: true, everyonePerms: true, ownerId: true, id: true, locked: true, status: true } as const satisfies Prisma.ProjectSelect - -export async function authUser(req: FastifyRequest): Promise -export async function authUser(req: FastifyRequest, projectUnique: ProjectUniqueFinder): Promise -export async function authUser(req: FastifyRequest, projectUnique?: ProjectUniqueFinder): Promise { - let adminPermissions: bigint = 0n - let tokenId: string | undefined - let user: UserDetails | undefined - - if (req.session.user) { - const loginResult = await logViaSession(req.session.user) - user = { - ...loginResult.user, - groups: req.session.user.groups, + { slug: string }, + XOR< + { environmentId: string }, + XOR<{ repositoryId: string }, { id: string }> + > +>; + +const projectPermsSelect = { + roles: true, + members: true, + everyonePerms: true, + ownerId: true, + id: true, + locked: true, + status: true, +} as const satisfies Prisma.ProjectSelect; + +export async function authUser(req: FastifyRequest): Promise; +export async function authUser( + req: FastifyRequest, + projectUnique: ProjectUniqueFinder, +): Promise; +export async function authUser( + req: FastifyRequest, + projectUnique?: ProjectUniqueFinder, +): Promise { + let adminPermissions: bigint = 0n; + let tokenId: string | undefined; + let user: UserDetails | undefined; + + if (req.session.user) { + const loginResult = await logViaSession(req.session.user); + user = { + ...loginResult.user, + groups: req.session.user.groups, + }; + adminPermissions = loginResult.adminPerms; + } else { + const tokenHeader = req.headers[tokenHeaderName]; + if (typeof tokenHeader === 'string') { + const resultToken = await logViaToken(tokenHeader); + if (typeof resultToken === 'string') { + throw new Unauthorized401(resultToken); + } + adminPermissions = resultToken.adminPerms ?? 0n; + tokenId = resultToken.user.tokenId; + if (!user && resultToken.user) { + user = { ...resultToken.user, groups: [] }; + } + } + } + + const baseReturnInfos = { + user, + adminPermissions, + tokenId, + }; + if (!projectUnique || !user) { + return baseReturnInfos; } - adminPermissions = loginResult.adminPerms - } else { - const tokenHeader = req.headers[tokenHeaderName] - if (typeof tokenHeader === 'string') { - const resultToken = await logViaToken(tokenHeader) - if (typeof resultToken === 'string') { - throw new Unauthorized401(resultToken) - } - adminPermissions = resultToken.adminPerms ?? 0n - tokenId = resultToken.user.tokenId - if (!user && resultToken.user) { - user = { ...resultToken.user, groups: [] } - } + let project: ProjectMinimalPerms | null | undefined; + + if (projectUnique.repositoryId) { + project = ( + await prisma.repository.findUnique({ + where: { id: projectUnique.repositoryId }, + select: { project: { select: projectPermsSelect } }, + }) + )?.project; + } else if (projectUnique.environmentId) { + project = ( + await prisma.environment.findUnique({ + where: { id: projectUnique.environmentId }, + select: { project: { select: projectPermsSelect } }, + }) + )?.project; + } else if (projectUnique.id) { + project = uuid.test(projectUnique.id) + ? await prisma.project.findUnique({ + where: { id: projectUnique.id }, + select: projectPermsSelect, + }) + : await prisma.project.findUnique({ + where: { slug: projectUnique.id }, + select: projectPermsSelect, + }); + } else if (projectUnique.slug) { + project = await prisma.project.findFirstOrThrow({ + where: { slug: projectUnique.slug }, + select: projectPermsSelect, + }); + } + if (!project) { + return baseReturnInfos; } - } - - const baseReturnInfos = { - user, - adminPermissions, - tokenId, - } - if (!projectUnique || !user) { - return baseReturnInfos - } - let project: ProjectMinimalPerms | null | undefined - - if (projectUnique.repositoryId) { - project = (await prisma.repository.findUnique({ - where: { id: projectUnique.repositoryId }, - select: { project: { select: projectPermsSelect } }, - }))?.project - } else if (projectUnique.environmentId) { - project = (await prisma.environment.findUnique({ - where: { id: projectUnique.environmentId }, - select: { project: { select: projectPermsSelect } }, - }))?.project - } else if (projectUnique.id) { - project = uuid.test(projectUnique.id) - ? await prisma.project.findUnique({ - where: { id: projectUnique.id }, - select: projectPermsSelect, - }) - : await prisma.project.findUnique({ - where: { slug: projectUnique.id }, - select: projectPermsSelect, - }) - } else if (projectUnique.slug) { - project = await prisma.project.findFirstOrThrow({ - where: { slug: projectUnique.slug }, - select: projectPermsSelect, - }) - } - if (!project) { - return baseReturnInfos - } - - const projectPermissions = getProjectPermissions(project, user) - - return { - user, - adminPermissions, - projectPermissions, - projectId: project.id, - projectLocked: project.locked, - projectStatus: project.status, - projectOwnerId: project.ownerId, - } -} -function getProjectPermissions(project: ProjectMinimalPerms, user: UserDetails): bigint | undefined { - if (project.ownerId === user.id) return PP.MANAGE - const member = project.members.find(member => member.userId === user.id) - if (!member) return + const projectPermissions = getProjectPermissions(project, user); + + return { + user, + adminPermissions, + projectPermissions, + projectId: project.id, + projectLocked: project.locked, + projectStatus: project.status, + projectOwnerId: project.ownerId, + }; +} - const memberRoles = project.roles.filter(role => member.roleIds.includes(role.id)) - return memberRoles.reduce((acc, curr) => acc | curr.permissions, project.everyonePerms | PROJECT_PERMS.GUEST) +function getProjectPermissions( + project: ProjectMinimalPerms, + user: UserDetails, +): bigint | undefined { + if (project.ownerId === user.id) return PP.MANAGE; + const member = project.members.find((member) => member.userId === user.id); + if (!member) return; + + const memberRoles = project.roles.filter((role) => + member.roleIds.includes(role.id), + ); + return memberRoles.reduce( + (acc, curr) => acc | curr.permissions, + project.everyonePerms | PROJECT_PERMS.GUEST, + ); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts index 7abcaa1aa..694a66517 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts @@ -1,15 +1,16 @@ -import { describe, expect, it } from 'vitest' -import { getJSDateFromUtcIso } from './date.js' +import { describe, expect, it } from 'vitest'; + +import { getJSDateFromUtcIso } from './date.js'; describe('date-util', () => { - it('should return a native Date object', () => { - const date = '2022-10-11' + it('should return a native Date object', () => { + const date = '2022-10-11'; - const received = getJSDateFromUtcIso(date) + const received = getJSDateFromUtcIso(date); - expect(received.getMonth()).toBe(9) - expect(received.getFullYear()).toBe(2022) - expect(received.getDate()).toBeGreaterThan(10) - expect(received.getDate()).toBeLessThan(12) - }) -}) + expect(received.getMonth()).toBe(9); + expect(received.getFullYear()).toBe(2022); + expect(received.getDate()).toBeGreaterThan(10); + expect(received.getDate()).toBeLessThan(12); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts index 87473d262..59e9c80b2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts @@ -1,5 +1,5 @@ -import { parseISO } from 'date-fns' +import { parseISO } from 'date-fns'; export function getJSDateFromUtcIso(dateUtcIso: string) { - return parseISO(dateUtcIso) + return parseISO(dateUtcIso); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts index fc41aab75..f38754a1d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts @@ -1,57 +1,61 @@ -import * as dotenv from 'dotenv' +import * as dotenv from 'dotenv'; if (process.env.DOCKER !== 'true') { - dotenv.config({ path: '.env' }) + dotenv.config({ path: '.env' }); } if (process.env.INTEGRATION === 'true') { - const envInteg = dotenv.config({ path: '.env.integ' }) - process.env = { - ...process.env, - ...(envInteg?.parsed ?? {}), - } + const envInteg = dotenv.config({ path: '.env.integ' }); + process.env = { + ...process.env, + ...(envInteg?.parsed ?? {}), + }; } // application mode -export const isDev = process.env.NODE_ENV === 'development' -export const isTest = process.env.NODE_ENV === 'test' -export const isProd = process.env.NODE_ENV === 'production' -export const isInt = process.env.INTEGRATION === 'true' -export const isCI = process.env.CI === 'true' -export const isDevSetup = process.env.DEV_SETUP === 'true' +export const isDev = process.env.NODE_ENV === 'development'; +export const isTest = process.env.NODE_ENV === 'test'; +export const isProd = process.env.NODE_ENV === 'production'; +export const isInt = process.env.INTEGRATION === 'true'; +export const isCI = process.env.CI === 'true'; +export const isDevSetup = process.env.DEV_SETUP === 'true'; // app -export const port = process.env.SERVER_PORT +export const port = process.env.SERVER_PORT; export const appVersion = isProd - ? (process.env.APP_VERSION ?? 'unknown') - : 'dev' + ? (process.env.APP_VERSION ?? 'unknown') + : 'dev'; // db -export const dbUrl = process.env.DB_URL +export const dbUrl = process.env.DB_URL; // keycloak -export const sessionSecret = process.env.SESSION_SECRET -export const keycloakProtocol = process.env.KEYCLOAK_PROTOCOL -export const keycloakDomain = process.env.KEYCLOAK_DOMAIN -export const keycloakRealm = process.env.KEYCLOAK_REALM -export const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID -export const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET -export const keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI +export const sessionSecret = process.env.SESSION_SECRET; +export const keycloakProtocol = process.env.KEYCLOAK_PROTOCOL; +export const keycloakDomain = process.env.KEYCLOAK_DOMAIN; +export const keycloakRealm = process.env.KEYCLOAK_REALM; +export const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID; +export const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET; +export const keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI; export const adminsUserId = process.env.ADMIN_KC_USER_ID - ? process.env.ADMIN_KC_USER_ID.split(',') - : [] + ? process.env.ADMIN_KC_USER_ID.split(',') + : []; -export const contactEmail = process.env.CONTACT_EMAIL ?? 'cloudpinative-relations@interieur.gouv.fr' +export const contactEmail = + process.env.CONTACT_EMAIL ?? 'cloudpinative-relations@interieur.gouv.fr'; // plugins -export const mockPlugins = process.env.MOCK_PLUGINS === 'true' -export const projectRootDir = process.env.PROJECTS_ROOT_DIR -export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins' -export const NODE_ENV = process.env.NODE_ENV === 'test' - ? 'test' - : process.env.NODE_ENV === 'development' - ? 'development' - : 'production' +export const mockPlugins = process.env.MOCK_PLUGINS === 'true'; +export const projectRootDir = process.env.PROJECTS_ROOT_DIR; +export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins'; +export const NODE_ENV = + process.env.NODE_ENV === 'test' + ? 'test' + : process.env.NODE_ENV === 'development' + ? 'development' + : 'production'; // server tuning -export const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT ? Number(process.env.PARALLEL_BULK_LIMIT) : 5 +export const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT + ? Number(process.env.PARALLEL_BULK_LIMIT) + : 5; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts index 0f1dd07fb..4f8750b5e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts @@ -1,48 +1,48 @@ export class ErrorResType { - readonly status: 400 | 401 | 403 | 404 | 422 | 500 - body: { message: string } = { message: '' } - constructor(code: 400 | 401 | 403 | 404 | 422 | 500) { - this.status = code - } + readonly status: 400 | 401 | 403 | 404 | 422 | 500; + body: { message: string } = { message: '' }; + constructor(code: 400 | 401 | 403 | 404 | 422 | 500) { + this.status = code; + } } export class BadRequest400 extends ErrorResType { - constructor(message: string) { - super(400) - this.body.message = message ?? 'Bad Request' - } + constructor(message: string) { + super(400); + this.body.message = message ?? 'Bad Request'; + } } export class Unauthorized401 extends ErrorResType { - constructor(message?: string) { - super(401) - this.body.message = message ?? 'Unauthorized' - } + constructor(message?: string) { + super(401); + this.body.message = message ?? 'Unauthorized'; + } } export class Forbidden403 extends ErrorResType { - constructor(message?: string) { - super(403) - this.body.message = message ?? 'Forbidden' - } + constructor(message?: string) { + super(403); + this.body.message = message ?? 'Forbidden'; + } } export class NotFound404 extends ErrorResType { - constructor() { - super(404) - this.body.message = 'Not Found' - } + constructor() { + super(404); + this.body.message = 'Not Found'; + } } export class Unprocessable422 extends ErrorResType { - constructor(message?: string) { - super(422) - this.body.message = message ?? 'Unprocessable Entity' - } + constructor(message?: string) { + super(422); + this.body.message = message ?? 'Unprocessable Entity'; + } } export class Internal500 extends ErrorResType { - constructor(message: string) { - super(500) - this.body.message = message - } + constructor(message: string) { + super(500); + this.body.message = message; + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts index 641977706..100a2b166 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts @@ -1,55 +1,56 @@ -import { randomUUID } from 'node:crypto' -import type { FastifyServerOptions } from 'fastify' -import type { generateOpenApi } from '@ts-rest/open-api' -import { swaggerUiPath } from '@cpn-console/shared' -import { loggerConf } from './logger.js' +import { swaggerUiPath } from '@cpn-console/shared'; +import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui'; +import type { generateOpenApi } from '@ts-rest/open-api'; +import type { FastifyServerOptions } from 'fastify'; +import { randomUUID } from 'node:crypto'; + import { - NODE_ENV, - appVersion, - keycloakClientId, - keycloakClientSecret, - keycloakRealm, - keycloakRedirectUri, -} from './env.js' -import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui' + NODE_ENV, + appVersion, + keycloakClientId, + keycloakClientSecret, + keycloakRealm, + keycloakRedirectUri, +} from './env.js'; +import { loggerConf } from './logger.js'; export const fastifyConf: FastifyServerOptions = { - maxParamLength: 5000, - logger: loggerConf[NODE_ENV] ?? loggerConf.production, - genReqId: () => randomUUID(), -} + maxParamLength: 5000, + logger: loggerConf[NODE_ENV] ?? loggerConf.production, + genReqId: () => randomUUID(), +}; const externalDocs = { - description: 'External documentation.', - url: 'https://cloud-pi-native.fr', -} + description: 'External documentation.', + url: 'https://cloud-pi-native.fr', +}; export const swaggerConf: Parameters[1] = { - info: { - title: 'Console Cloud Pi Native', - description: 'API de gestion des ressources Cloud Pi Native.', - version: appVersion, - }, - - externalDocs, - servers: [ - { - url: keycloakRedirectUri, + info: { + title: 'Console Cloud Pi Native', + description: 'API de gestion des ressources Cloud Pi Native.', + version: appVersion, }, - ], -} + + externalDocs, + servers: [ + { + url: keycloakRedirectUri, + }, + ], +}; export const swaggerUiConf: FastifySwaggerUiOptions = { - routePrefix: swaggerUiPath, - uiConfig: { - docExpansion: 'list', - deepLinking: false, - }, - initOAuth: { - clientId: keycloakClientId, - clientSecret: keycloakClientSecret, - realm: keycloakRealm, - appName: 'Cloud Pi Native', - scopes: 'openid generic', - }, -} + routePrefix: swaggerUiPath, + uiConfig: { + docExpansion: 'list', + deepLinking: false, + }, + initOAuth: { + clientId: keycloakClientId, + clientSecret: keycloakClientSecret, + realm: keycloakRealm, + appName: 'Cloud Pi Native', + scopes: 'openid generic', + }, +}; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts index d7d1bdc3a..c198f5176 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts @@ -1,235 +1,265 @@ -import type { KubeCluster, KubeUser, Project as ProjectPayload, Store } from '@cpn-console/hooks' -import { describe, expect, it } from 'vitest' -import type { ProjectInfos, ReposCreds } from './hook-wrapper.ts' -import { transformToHookProject } from './hook-wrapper.ts' +import type { + KubeCluster, + KubeUser, + Project as ProjectPayload, + Store, +} from '@cpn-console/hooks'; +import { describe, expect, it } from 'vitest'; + +import type { ProjectInfos, ReposCreds } from './hook-wrapper.ts'; +import { transformToHookProject } from './hook-wrapper.ts'; const associatedCluster = { - id: 'f0e39981-0b6d-4c16-aa96-225062b75767', - infos: '', - label: 'carno', - privacy: 'dedicated', - secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9b60', - kubeconfig: { - id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1437', - user: { - keyData: 'REDACTED', - certData: 'REDACTED', + id: 'f0e39981-0b6d-4c16-aa96-225062b75767', + infos: '', + label: 'carno', + privacy: 'dedicated', + secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9b60', + kubeconfig: { + id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1437', + user: { + keyData: 'REDACTED', + certData: 'REDACTED', + }, + cluster: { + caData: 'REDACTED', + server: 'https://api-server:6443', + skipTLSVerify: false, + tlsServerName: 'api-server', + }, + createdAt: '2024-05-02T09:17:27.882Z', + updatedAt: '2024-05-02T09:17:27.882Z', }, - cluster: { - caData: 'REDACTED', - server: 'https://api-server:6443', - skipTLSVerify: false, - tlsServerName: 'api-server', + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', + slug: 'default', }, - createdAt: '2024-05-02T09:17:27.882Z', - updatedAt: '2024-05-02T09:17:27.882Z', - }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', - slug: 'default', - }, -} +}; const nonAssociatedCluster = { - id: 'f0e39981-0b6d-4c16-aa96-225062b75111', - infos: '', - label: 'carno2', - privacy: 'dedicated', - secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9111', - kubeconfig: { - id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1111', - user: { - keyData: 'REDACTED', - certData: 'REDACTED', + id: 'f0e39981-0b6d-4c16-aa96-225062b75111', + infos: '', + label: 'carno2', + privacy: 'dedicated', + secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9111', + kubeconfig: { + id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1111', + user: { + keyData: 'REDACTED', + certData: 'REDACTED', + }, + cluster: { + caData: 'REDACTED', + server: 'https://api-server:6443', + skipTLSVerify: false, + tlsServerName: 'api-server', + }, + createdAt: '2024-05-02T09:17:27.882Z', + updatedAt: '2024-05-02T09:17:27.882Z', }, - cluster: { - caData: 'REDACTED', - server: 'https://api-server:6443', - skipTLSVerify: false, - tlsServerName: 'api-server', + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', + slug: 'default', }, - createdAt: '2024-05-02T09:17:27.882Z', - updatedAt: '2024-05-02T09:17:27.882Z', - }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', - slug: 'default', - }, -} +}; const project: ProjectInfos = { - id: '011e7860-04d7-461f-912d-334c622d38b3', - name: 'candilib', - description: 'Application de réservation de places à l\'examen du permis B.', - status: 'created', - locked: false, - createdAt: '2023-07-03T14:46:56.778Z', - updatedAt: '2023-07-03T14:46:56.783Z', - everyonePerms: 896n, - ownerId: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', - members: [], - clusters: [associatedCluster, nonAssociatedCluster], - environments: [ - { - id: '1b9f1053-fcf5-4053-a7b2-ff8a2c0c1921', - name: 'dev', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - createdAt: '2023-07-03T14:46:56.787Z', - updatedAt: '2023-07-03T14:46:56.803Z', - clusterId: 'aaaaaaaa-5b03-45d5-847b-149dec875680', - quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', - stageId: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', - quota: { - id: '5a57b62f-2465-4fb6-a853-5a751d099199', - memory: '4Gi', - cpu: 2, - name: 'small', - isPrivate: false, - }, - stage: { - id: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', - name: 'dev', - }, - cluster: { - id: 'aaaaaaaa-5b03-45d5-847b-149dec875680', - infos: 'Floating IP : 0.0.0.0', - label: 'pas-top-cluster', - privacy: 'dedicated', - secretName: '94d52618-7869-4192-b33e-85dd0959e815', - kubeconfig: { - id: 'b5662039-a62b-483e-ba54-b12c6f966c96', - user: { - token: 'kirikou', - }, - cluster: { - server: 'https://pwned.cluster', - tlsServerName: 'pwned.cluster', - }, - createdAt: '2024-07-24T16:54:14.969Z', - updatedAt: '2024-07-24T16:54:14.969Z', + id: '011e7860-04d7-461f-912d-334c622d38b3', + name: 'candilib', + description: "Application de réservation de places à l'examen du permis B.", + status: 'created', + locked: false, + createdAt: '2023-07-03T14:46:56.778Z', + updatedAt: '2023-07-03T14:46:56.783Z', + everyonePerms: 896n, + ownerId: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', + members: [], + clusters: [associatedCluster, nonAssociatedCluster], + environments: [ + { + id: '1b9f1053-fcf5-4053-a7b2-ff8a2c0c1921', + name: 'dev', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + createdAt: '2023-07-03T14:46:56.787Z', + updatedAt: '2023-07-03T14:46:56.803Z', + clusterId: 'aaaaaaaa-5b03-45d5-847b-149dec875680', + quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', + stageId: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', + quota: { + id: '5a57b62f-2465-4fb6-a853-5a751d099199', + memory: '4Gi', + cpu: 2, + name: 'small', + isPrivate: false, + }, + stage: { + id: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', + name: 'dev', + }, + cluster: { + id: 'aaaaaaaa-5b03-45d5-847b-149dec875680', + infos: 'Floating IP : 0.0.0.0', + label: 'pas-top-cluster', + privacy: 'dedicated', + secretName: '94d52618-7869-4192-b33e-85dd0959e815', + kubeconfig: { + id: 'b5662039-a62b-483e-ba54-b12c6f966c96', + user: { + token: 'kirikou', + }, + cluster: { + server: 'https://pwned.cluster', + tlsServerName: 'pwned.cluster', + }, + createdAt: '2024-07-24T16:54:14.969Z', + updatedAt: '2024-07-24T16:54:14.969Z', + }, + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', + slug: 'pub', + }, + }, }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', - slug: 'pub', + { + id: '1c654f00-4798-4a80-929f-960ddb37885a', + name: 'integration', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + createdAt: '2023-07-03T14:46:56.788Z', + updatedAt: '2023-07-03T14:46:56.803Z', + clusterId: '126ac57f-263c-4463-87bb-d4e9017056b2', + quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', + stageId: 'd434310e-7850-4d59-b47f-0772edf50582', + quota: { + id: '5a57b62f-2465-4fb6-a853-5a751d099199', + memory: '4Gi', + cpu: 2, + name: 'small', + isPrivate: false, + }, + stage: { + id: 'd434310e-7850-4d59-b47f-0772edf50582', + name: 'integration', + }, + cluster: { + id: '126ac57f-263c-4463-87bb-d4e9017056b2', + infos: null, + label: 'top-secret-cluster', + privacy: 'dedicated', + secretName: '59be2d50-58f9-42f3-95dc-b0c0518e3d8a', + kubeconfig: { + id: '0e88f000-07e6-4781-a69d-0963489387f7', + user: { + token: 'nyan cat', + }, + cluster: { + server: 'https://nothere.cluster', + skipTLSVerify: false, + tlsServerName: 'nothere.cluster', + }, + createdAt: '2024-07-24T16:54:14.966Z', + updatedAt: '2024-07-24T16:54:14.966Z', + }, + clusterResources: true, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', + slug: 'pub', + }, + }, }, - }, - }, - { - id: '1c654f00-4798-4a80-929f-960ddb37885a', - name: 'integration', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - createdAt: '2023-07-03T14:46:56.788Z', - updatedAt: '2023-07-03T14:46:56.803Z', - clusterId: '126ac57f-263c-4463-87bb-d4e9017056b2', - quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', - stageId: 'd434310e-7850-4d59-b47f-0772edf50582', - quota: { - id: '5a57b62f-2465-4fb6-a853-5a751d099199', - memory: '4Gi', - cpu: 2, - name: 'small', - isPrivate: false, - }, - stage: { - id: 'd434310e-7850-4d59-b47f-0772edf50582', - name: 'integration', - }, - cluster: { - id: '126ac57f-263c-4463-87bb-d4e9017056b2', - infos: null, - label: 'top-secret-cluster', - privacy: 'dedicated', - secretName: '59be2d50-58f9-42f3-95dc-b0c0518e3d8a', - kubeconfig: { - id: '0e88f000-07e6-4781-a69d-0963489387f7', - user: { - token: 'nyan cat', - }, - cluster: { - server: 'https://nothere.cluster', - skipTLSVerify: false, - tlsServerName: 'nothere.cluster', - }, - createdAt: '2024-07-24T16:54:14.966Z', - updatedAt: '2024-07-24T16:54:14.966Z', - }, - clusterResources: true, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', - slug: 'pub', + ], + repositories: [ + { + id: '299216bb-2dcc-42b5-ac71-6aa001d2dccf', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + internalRepoName: 'candilib', + externalRepoUrl: 'https://github.com/dnum-mi/candilib.git', + externalUserName: 'this-is-a-test', + isInfra: false, + isPrivate: true, + createdAt: '2023-07-03T14:46:56.788Z', + updatedAt: '2023-07-03T14:46:56.802Z', }, - }, + ], + plugins: [], + owner: { + id: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', + firstName: 'Jean', + lastName: 'DUPOND', + email: 'test@test.com', + createdAt: '2023-07-03T14:46:56.770Z', + updatedAt: '2023-07-03T14:46:56.770Z', + adminRoleIds: [], }, - ], - repositories: [ - { - id: '299216bb-2dcc-42b5-ac71-6aa001d2dccf', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - internalRepoName: 'candilib', - externalRepoUrl: 'https://github.com/dnum-mi/candilib.git', - externalUserName: 'this-is-a-test', - isInfra: false, - isPrivate: true, - createdAt: '2023-07-03T14:46:56.788Z', - updatedAt: '2023-07-03T14:46:56.802Z', - }, - ], - plugins: [], - owner: { - id: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', - firstName: 'Jean', - lastName: 'DUPOND', - email: 'test@test.com', - createdAt: '2023-07-03T14:46:56.770Z', - updatedAt: '2023-07-03T14:46:56.770Z', - adminRoleIds: [], - }, - roles: [], -} + roles: [], +}; describe('transformToHookProject', () => { - // Mock data - const mockStore: Store = {} - const mockReposCreds: ReposCreds = { - console: { - token: 'test', - username: 'test', - }, - } + // Mock data + const mockStore: Store = {}; + const mockReposCreds: ReposCreds = { + console: { + token: 'test', + username: 'test', + }, + }; - it('transforme correctement le projet en objet Payload', () => { - const result: ProjectPayload = transformToHookProject(project, mockStore, mockReposCreds) + it('transforme correctement le projet en objet Payload', () => { + const result: ProjectPayload = transformToHookProject( + project, + mockStore, + mockReposCreds, + ); - // Asserts pour vérifier la transformation + // Asserts pour vérifier la transformation - // Assert sur la transformation des utilisateurs - expect(result.users).toEqual([project.owner]) + // Assert sur la transformation des utilisateurs + expect(result.users).toEqual([project.owner]); - // Assert sur la transformation des rôles - expect(result.roles).toEqual([{ userId: project.owner.id, role: 'owner' }]) + // Assert sur la transformation des rôles + expect(result.roles).toEqual([ + { userId: project.owner.id, role: 'owner' }, + ]); - // Assert sur la transformation des clusters - expect(result.clusters).toEqual([associatedCluster, nonAssociatedCluster].map(({ kubeconfig, ...cluster }) => ({ - user: kubeconfig.user as unknown as KubeUser, - cluster: kubeconfig.cluster as unknown as KubeCluster, - ...cluster, - privacy: cluster.privacy, - }))) + // Assert sur la transformation des clusters + expect(result.clusters).toEqual( + [associatedCluster, nonAssociatedCluster].map( + ({ kubeconfig, ...cluster }) => ({ + user: kubeconfig.user as unknown as KubeUser, + cluster: kubeconfig.cluster as unknown as KubeCluster, + ...cluster, + privacy: cluster.privacy, + }), + ), + ); - // Assert sur la transformation des environnements - expect(result.environments).toEqual(project.environments.map(({ permissions: _, stage, quota, ...environment }) => ({ - quota, - stage: stage.name, - permissions: [{ permissions: { rw: true, ro: true }, userId: project.ownerId }], - ...environment, - apis: {}, - }))) + // Assert sur la transformation des environnements + expect(result.environments).toEqual( + project.environments.map( + ({ permissions: _, stage, quota, ...environment }) => ({ + quota, + stage: stage.name, + permissions: [ + { + permissions: { rw: true, ro: true }, + userId: project.ownerId, + }, + ], + ...environment, + apis: {}, + }), + ), + ); - // Assert sur la transformation des repositories - expect(result.repositories).toEqual(project.repositories.map(repo => ({ ...repo, newCreds: mockReposCreds[repo.internalRepoName] }))) + // Assert sur la transformation des repositories + expect(result.repositories).toEqual( + project.repositories.map((repo) => ({ + ...repo, + newCreds: mockReposCreds[repo.internalRepoName], + })), + ); - // Assert sur le store - expect(result.store).toEqual(mockStore) - }) -}) + // Assert sur le store + expect(result.store).toEqual(mockStore); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts index 06c3e4ab9..8e0373145 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts @@ -1,231 +1,371 @@ -import type { Cluster, Kubeconfig, Project, ProjectRole, Zone } from '@prisma/client' -import type { ClusterObject, HookResult, KubeCluster, KubeUser, Project as ProjectPayload, RepoCreds, Repository, Store, ZoneObject } from '@cpn-console/hooks' -import { hooks } from '@cpn-console/hooks' -import type { AsyncReturnType } from '@cpn-console/shared' -import { ProjectAuthorized, getPermsByUserRoles, resourceListToDict } from '@cpn-console/shared' -import { genericProxy } from './proxy.js' -import { archiveProject, getAdminPlugin, getClusterByIdOrThrow, getClusterNamesByZoneId, getClustersAssociatedWithProject, getHookProjectInfos, getHookRepository, getProjectStore, getZoneByIdOrThrow, saveProjectStore, updateProjectClusterHistory, updateProjectCreated, updateProjectFailed, updateProjectWarning } from '@old-server/resources/queries-index.js' -import type { ConfigRecords } from '@old-server/resources/project-service/business.js' -import { dbToObj } from '@old-server/resources/project-service/business.js' - -export type ReposCreds = Record -export type ProjectInfos = AsyncReturnType - -async function getProjectPayload(projectId: Project['id'], reposCreds?: ReposCreds) { - const [ - project, - store, - clusters, - ] = await Promise.all([ - getHookProjectInfos(projectId), - getProjectStore(projectId), - getClustersAssociatedWithProject(projectId), - ]) - - return transformToHookProject({ - ...project, - clusters, - }, dbToObj(store), reposCreds) -} +import type { + ClusterObject, + HookResult, + KubeCluster, + KubeUser, + Project as ProjectPayload, + RepoCreds, + Repository, + Store, + ZoneObject, +} from '@cpn-console/hooks'; +import { hooks } from '@cpn-console/hooks'; +import type { AsyncReturnType } from '@cpn-console/shared'; +import { + ProjectAuthorized, + getPermsByUserRoles, + resourceListToDict, +} from '@cpn-console/shared'; +import type { ConfigRecords } from '@old-server/resources/project-service/business.js'; +import { dbToObj } from '@old-server/resources/project-service/business.js'; +import { + archiveProject, + getAdminPlugin, + getClusterByIdOrThrow, + getClusterNamesByZoneId, + getClustersAssociatedWithProject, + getHookProjectInfos, + getHookRepository, + getProjectStore, + getZoneByIdOrThrow, + saveProjectStore, + updateProjectClusterHistory, + updateProjectCreated, + updateProjectFailed, + updateProjectWarning, +} from '@old-server/resources/queries-index.js'; +import type { + Cluster, + Kubeconfig, + Project, + ProjectRole, + Zone, +} from '@prisma/client'; -async function upsertProject(projectId: Project['id'], reposCreds?: ReposCreds) { - const [payload, config] = await Promise.all([ - getProjectPayload(projectId, reposCreds), - getAdminPlugin(), - ]) +import { genericProxy } from './proxy.js'; - const results = await hooks.upsertProject.execute(payload, dbToObj(config)) +export type ReposCreds = Record; +export type ProjectInfos = AsyncReturnType; - const records: ConfigRecords = Object.entries(results.results).reduce((acc, [pluginName, result]) => { - if (result.store) { - return [...acc, ...Object.entries(result.store).map(([key, value]) => ({ pluginName, key, value: String(value) }))] - } - return acc - }, [] as ConfigRecords) - - await saveProjectStore(records, projectId) - const project = await manageProjectStatus(projectId, results, 'upsert', payload.environments.map(env => env.clusterId)) - return { - results, - project, - } +async function getProjectPayload( + projectId: Project['id'], + reposCreds?: ReposCreds, +) { + const [project, store, clusters] = await Promise.all([ + getHookProjectInfos(projectId), + getProjectStore(projectId), + getClustersAssociatedWithProject(projectId), + ]); + + return transformToHookProject( + { + ...project, + clusters, + }, + dbToObj(store), + reposCreds, + ); } -const project = { - upsert: async (projectId: Project['id'], reposCreds?: ReposCreds) => { - const results = await upsertProject(projectId, reposCreds) - // automatically retry one time if it fails - return results.results.failed ? upsertProject(projectId, reposCreds) : results - }, - delete: async (projectId: Project['id']) => { + +async function upsertProject( + projectId: Project['id'], + reposCreds?: ReposCreds, +) { const [payload, config] = await Promise.all([ - getProjectPayload(projectId), - getAdminPlugin(), - ]) - const results = await hooks.deleteProject.execute(payload, dbToObj(config)) + getProjectPayload(projectId, reposCreds), + getAdminPlugin(), + ]); + + const results = await hooks.upsertProject.execute(payload, dbToObj(config)); + + const records: ConfigRecords = Object.entries(results.results).reduce( + (acc, [pluginName, result]) => { + if (result.store) { + return [ + ...acc, + ...Object.entries(result.store).map(([key, value]) => ({ + pluginName, + key, + value: String(value), + })), + ]; + } + return acc; + }, + [] as ConfigRecords, + ); + + await saveProjectStore(records, projectId); + const project = await manageProjectStatus( + projectId, + results, + 'upsert', + payload.environments.map((env) => env.clusterId), + ); return { - results, - project: await manageProjectStatus(projectId, results, 'delete', []), + results, + project, + }; +} +const project = { + upsert: async (projectId: Project['id'], reposCreds?: ReposCreds) => { + const results = await upsertProject(projectId, reposCreds); + // automatically retry one time if it fails + return results.results.failed + ? upsertProject(projectId, reposCreds) + : results; + }, + delete: async (projectId: Project['id']) => { + const [payload, config] = await Promise.all([ + getProjectPayload(projectId), + getAdminPlugin(), + ]); + const results = await hooks.deleteProject.execute( + payload, + dbToObj(config), + ); + return { + results, + project: await manageProjectStatus( + projectId, + results, + 'delete', + [], + ), + }; + }, + getSecrets: async (projectId: Project['id']) => { + const project = await getHookProjectInfos(projectId); + const store = dbToObj(await getProjectStore(project.id)); + const config = dbToObj(await getAdminPlugin()); + + return hooks.getProjectSecrets.execute({ ...project, store }, config); + }, +} as const; + +type ProjectAction = keyof typeof project; +async function manageProjectStatus( + projectId: Project['id'], + hookReply: HookResult, + action: ProjectAction, + envClusterIds: Cluster['id'][], +): Promise> { + if (!hookReply.failed && hookReply.results?.kubernetes) { + await updateProjectClusterHistory(projectId, envClusterIds); } - }, - getSecrets: async (projectId: Project['id']) => { - const project = await getHookProjectInfos(projectId) - const store = dbToObj(await getProjectStore(project.id)) - const config = dbToObj(await getAdminPlugin()) - - return hooks.getProjectSecrets.execute({ ...project, store }, config) - }, -} as const - -type ProjectAction = keyof typeof project -async function manageProjectStatus(projectId: Project['id'], hookReply: HookResult, action: ProjectAction, envClusterIds: Cluster['id'][]): Promise> { - if (!hookReply.failed && hookReply.results?.kubernetes) { - await updateProjectClusterHistory(projectId, envClusterIds) - } - if (hookReply.failed) { - return updateProjectFailed(projectId) - } else if (hookReply.warning.length) { - return updateProjectWarning(projectId) - } else if (action === 'upsert') { - return updateProjectCreated(projectId) - } else if (action === 'delete') { - return archiveProject(projectId) - } - throw new Error('unknown action') + if (hookReply.failed) { + return updateProjectFailed(projectId); + } else if (hookReply.warning.length) { + return updateProjectWarning(projectId); + } else if (action === 'upsert') { + return updateProjectCreated(projectId); + } else if (action === 'delete') { + return archiveProject(projectId); + } + throw new Error('unknown action'); } const cluster = { - upsert: async (clusterId: Cluster['id'], previousZoneId: Cluster['zoneId']) => { - const cluster = await getClusterByIdOrThrow(clusterId) - const clusterObject = cluster as unknown as ClusterObject - const store = dbToObj(await getAdminPlugin()) - if (cluster.zoneId !== previousZoneId) { - // Upsert on the old zone to remove cluster - const previousClusterObject = { - ...cluster, - } as unknown as ClusterObject - previousClusterObject.zone = await getZoneByIdOrThrow(previousZoneId) - previousClusterObject.zone.clusterNames = await getClusterNamesByZoneId(previousZoneId) - const hookResult = await hooks.upsertCluster.execute({ - ...cluster.kubeconfig as unknown as Pick, - ...previousClusterObject, - }, store) - if (hookResult.failed) { - return hookResult - } - } - clusterObject.zone.clusterNames = await getClusterNamesByZoneId(cluster.zoneId) - return hooks.upsertCluster.execute({ - ...cluster.kubeconfig as unknown as Pick, - ...clusterObject, - }, store) - }, - delete: async (clusterId: Cluster['id']) => { - const cluster = await getClusterByIdOrThrow(clusterId) - const clusterObject = cluster as unknown as ClusterObject - const clusterNames = await getClusterNamesByZoneId(cluster.zoneId) - clusterObject.zone.clusterNames = clusterNames.filter(c => c !== cluster.label) - const store = dbToObj(await getAdminPlugin()) - return hooks.deleteCluster.execute({ - ...cluster.kubeconfig as unknown as ClusterObject, - ...clusterObject, - }, store) - }, -} as const + upsert: async ( + clusterId: Cluster['id'], + previousZoneId: Cluster['zoneId'], + ) => { + const cluster = await getClusterByIdOrThrow(clusterId); + const clusterObject = cluster as unknown as ClusterObject; + const store = dbToObj(await getAdminPlugin()); + if (cluster.zoneId !== previousZoneId) { + // Upsert on the old zone to remove cluster + const previousClusterObject = { + ...cluster, + } as unknown as ClusterObject; + previousClusterObject.zone = + await getZoneByIdOrThrow(previousZoneId); + previousClusterObject.zone.clusterNames = + await getClusterNamesByZoneId(previousZoneId); + const hookResult = await hooks.upsertCluster.execute( + { + ...(cluster.kubeconfig as unknown as Pick< + ClusterObject, + 'cluster' | 'user' + >), + ...previousClusterObject, + }, + store, + ); + if (hookResult.failed) { + return hookResult; + } + } + clusterObject.zone.clusterNames = await getClusterNamesByZoneId( + cluster.zoneId, + ); + return hooks.upsertCluster.execute( + { + ...(cluster.kubeconfig as unknown as Pick< + ClusterObject, + 'cluster' | 'user' + >), + ...clusterObject, + }, + store, + ); + }, + delete: async (clusterId: Cluster['id']) => { + const cluster = await getClusterByIdOrThrow(clusterId); + const clusterObject = cluster as unknown as ClusterObject; + const clusterNames = await getClusterNamesByZoneId(cluster.zoneId); + clusterObject.zone.clusterNames = clusterNames.filter( + (c) => c !== cluster.label, + ); + const store = dbToObj(await getAdminPlugin()); + return hooks.deleteCluster.execute( + { + ...(cluster.kubeconfig as unknown as ClusterObject), + ...clusterObject, + }, + store, + ); + }, +} as const; const user = { - retrieveUserByEmail: async (email: string) => { - const config = dbToObj(await getAdminPlugin()) - return hooks.retrieveUserByEmail.execute({ email }, config) - }, -} as const + retrieveUserByEmail: async (email: string) => { + const config = dbToObj(await getAdminPlugin()); + return hooks.retrieveUserByEmail.execute({ email }, config); + }, +} as const; const zone = { - upsert: async (zoneId: Zone['id']) => { - const zone: ZoneObject = await getZoneByIdOrThrow(zoneId) - zone.clusterNames = await getClusterNamesByZoneId(zoneId) - const store = dbToObj(await getAdminPlugin()) - return hooks.upsertZone.execute(zone, store) - }, - delete: async (zoneId: Zone['id']) => { - const zone = await getZoneByIdOrThrow(zoneId) - const store = dbToObj(await getAdminPlugin()) - return hooks.deleteZone.execute(zone, store) - }, -} as const + upsert: async (zoneId: Zone['id']) => { + const zone: ZoneObject = await getZoneByIdOrThrow(zoneId); + zone.clusterNames = await getClusterNamesByZoneId(zoneId); + const store = dbToObj(await getAdminPlugin()); + return hooks.upsertZone.execute(zone, store); + }, + delete: async (zoneId: Zone['id']) => { + const zone = await getZoneByIdOrThrow(zoneId); + const store = dbToObj(await getAdminPlugin()); + return hooks.deleteZone.execute(zone, store); + }, +} as const; const misc = { - checkServices: async () => { - const config = dbToObj(await getAdminPlugin()) - return hooks.checkServices.execute({}, config) - }, - syncRepository: async (repoId: string, { syncAllBranches, branchName }: { syncAllBranches: boolean, branchName?: string }) => { - const { project, ...repoInfos } = await getHookRepository(repoId) - const store = dbToObj(await getProjectStore(project.id)) - const payload = { - repo: { ...repoInfos, syncAllBranches, branchName }, - ...project, - store, - } - const config = dbToObj(await getAdminPlugin()) - return hooks.syncRepository.execute(payload, config) - }, -} as const + checkServices: async () => { + const config = dbToObj(await getAdminPlugin()); + return hooks.checkServices.execute({}, config); + }, + syncRepository: async ( + repoId: string, + { + syncAllBranches, + branchName, + }: { syncAllBranches: boolean; branchName?: string }, + ) => { + const { project, ...repoInfos } = await getHookRepository(repoId); + const store = dbToObj(await getProjectStore(project.id)); + const payload = { + repo: { ...repoInfos, syncAllBranches, branchName }, + ...project, + store, + }; + const config = dbToObj(await getAdminPlugin()); + return hooks.syncRepository.execute(payload, config); + }, +} as const; export const hook = { - // @ts-ignore TODO voir comment opti la signature de la fonction - misc: genericProxy(misc), - // @ts-ignore TODO voir comment opti la signature de la fonction - project: genericProxy(project, { upsert: ['delete'], delete: ['upsert', 'delete'], getSecrets: ['delete'] }), - // @ts-ignore TODO voir comment opti la signature de la fonction - cluster: genericProxy(cluster, { delete: ['upsert', 'delete'], upsert: ['delete'] }), - // @ts-ignore TODO voir comment opti la signature de la fonction - zone: genericProxy(zone, { delete: ['upsert'], upsert: ['delete'] }), - // @ts-ignore TODO voir comment opti la signature de la fonction - user: genericProxy(user, {}), -} + // @ts-ignore TODO voir comment opti la signature de la fonction + misc: genericProxy(misc), + // @ts-ignore TODO voir comment opti la signature de la fonction + project: genericProxy(project, { + upsert: ['delete'], + delete: ['upsert', 'delete'], + getSecrets: ['delete'], + }), + // @ts-ignore TODO voir comment opti la signature de la fonction + cluster: genericProxy(cluster, { + delete: ['upsert', 'delete'], + upsert: ['delete'], + }), + // @ts-ignore TODO voir comment opti la signature de la fonction + zone: genericProxy(zone, { delete: ['upsert'], upsert: ['delete'] }), + // @ts-ignore TODO voir comment opti la signature de la fonction + user: genericProxy(user, {}), +}; -function formatClusterInfos({ kubeconfig, ...cluster }: Omit - & { kubeconfig: Kubeconfig, zone: Pick }) { - return { - user: kubeconfig.user as unknown as KubeUser, - cluster: kubeconfig.cluster as unknown as KubeCluster, - ...cluster, - privacy: cluster.privacy, - } +function formatClusterInfos({ + kubeconfig, + ...cluster +}: Omit & { + kubeconfig: Kubeconfig; + zone: Pick; +}) { + return { + user: kubeconfig.user as unknown as KubeUser, + cluster: kubeconfig.cluster as unknown as KubeCluster, + ...cluster, + privacy: cluster.privacy, + }; } -export type RolesById = Record - -export function transformToHookProject(project: ProjectInfos, store: Store, reposCreds: ReposCreds = {}): ProjectPayload { - const clusters = project.clusters.map(cluster => formatClusterInfos(cluster)) - const rolesById = resourceListToDict(project.roles) - - return ({ - ...project, - clusters, - environments: project.environments.map(({ stage, ...environment }) => ({ - stage: stage.name, - permissions: [ - { permissions: { rw: true, ro: true }, userId: project.ownerId }, - ...project.members.map(member => ({ - userId: member.userId, - permissions: { - ro: ProjectAuthorized.ListEnvironments({ adminPermissions: 0n, projectPermissions: getPermsByUserRoles(member.roleIds, rolesById, project.everyonePerms) }), - rw: ProjectAuthorized.ManageEnvironments({ adminPermissions: 0n, projectPermissions: getPermsByUserRoles(member.roleIds, rolesById, project.everyonePerms) }), - }, +export type RolesById = Record; + +export function transformToHookProject( + project: ProjectInfos, + store: Store, + reposCreds: ReposCreds = {}, +): ProjectPayload { + const clusters = project.clusters.map((cluster) => + formatClusterInfos(cluster), + ); + const rolesById = resourceListToDict(project.roles); + + return { + ...project, + clusters, + environments: project.environments.map(({ stage, ...environment }) => ({ + stage: stage.name, + permissions: [ + { + permissions: { rw: true, ro: true }, + userId: project.ownerId, + }, + ...project.members.map((member) => ({ + userId: member.userId, + permissions: { + ro: ProjectAuthorized.ListEnvironments({ + adminPermissions: 0n, + projectPermissions: getPermsByUserRoles( + member.roleIds, + rolesById, + project.everyonePerms, + ), + }), + rw: ProjectAuthorized.ManageEnvironments({ + adminPermissions: 0n, + projectPermissions: getPermsByUserRoles( + member.roleIds, + rolesById, + project.everyonePerms, + ), + }), + }, + })), + ], + ...environment, + apis: {}, + })), + repositories: project.repositories.map((repo) => ({ + ...repo, + newCreds: reposCreds[repo.internalRepoName], })), - ], - ...environment, - apis: {}, - })), - repositories: project.repositories.map(repo => ({ ...repo, newCreds: reposCreds[repo.internalRepoName] })), - store, - users: [project.owner, ...project.members.map(({ user }) => user)], - roles: [ - { userId: project.ownerId, role: 'owner' }, - ...project.members.map(member => ({ - userId: member.userId, - role: 'user' as const, - })), - ], - }) + store, + users: [project.owner, ...project.members.map(({ user }) => user)], + roles: [ + { userId: project.ownerId, role: 'owner' }, + ...project.members.map((member) => ({ + userId: member.userId, + role: 'user' as const, + })), + ], + }; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts index 42c9cfae0..ccdf1a23b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts @@ -1,45 +1,46 @@ -import { describe, expect, it } from 'vitest' -import { userPayloadMapper } from './keycloak-utils.js' +import { describe, expect, it } from 'vitest'; + +import { userPayloadMapper } from './keycloak-utils.js'; describe('keycloak', () => { - it('should map keycloak user object to DSO user object without groups', () => { - const payload = { - sub: 'thisIsAnId', - email: 'test@test.com', - given_name: 'Jean', - family_name: 'DUPOND', - } - const desired = { - id: 'thisIsAnId', - email: 'test@test.com', - firstName: 'Jean', - lastName: 'DUPOND', - groups: [], - } + it('should map keycloak user object to DSO user object without groups', () => { + const payload = { + sub: 'thisIsAnId', + email: 'test@test.com', + given_name: 'Jean', + family_name: 'DUPOND', + }; + const desired = { + id: 'thisIsAnId', + email: 'test@test.com', + firstName: 'Jean', + lastName: 'DUPOND', + groups: [], + }; - const transformed = userPayloadMapper(payload) + const transformed = userPayloadMapper(payload); - expect(transformed).toMatchObject(desired) - }) + expect(transformed).toMatchObject(desired); + }); - it('should map keycloak user object to DSO user object with groups', () => { - const payload = { - sub: 'thisIsAnId', - email: 'test@test.com', - given_name: 'Jean', - family_name: 'DUPOND', - groups: ['group1'], - } - const desired = { - id: 'thisIsAnId', - email: 'test@test.com', - firstName: 'Jean', - lastName: 'DUPOND', - groups: ['group1'], - } + it('should map keycloak user object to DSO user object with groups', () => { + const payload = { + sub: 'thisIsAnId', + email: 'test@test.com', + given_name: 'Jean', + family_name: 'DUPOND', + groups: ['group1'], + }; + const desired = { + id: 'thisIsAnId', + email: 'test@test.com', + firstName: 'Jean', + lastName: 'DUPOND', + groups: ['group1'], + }; - const transformed = userPayloadMapper(payload) + const transformed = userPayloadMapper(payload); - expect(transformed).toMatchObject(desired) - }) -}) + expect(transformed).toMatchObject(desired); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts index 462116029..870111176 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts @@ -1,27 +1,27 @@ -import { tokenHeaderName } from '@cpn-console/shared' -import type { FastifyRequest } from 'fastify' +import { tokenHeaderName } from '@cpn-console/shared'; +import type { FastifyRequest } from 'fastify'; interface KeycloakPayload { - sub: string - email: string - given_name: string - family_name: string - groups: string[] + sub: string; + email: string; + given_name: string; + family_name: string; + groups: string[]; } export function userPayloadMapper(userPayload: KeycloakPayload) { - return { - id: userPayload.sub, - email: userPayload.email, - firstName: userPayload.given_name, - lastName: userPayload.family_name, - groups: userPayload.groups || [], - } + return { + id: userPayload.sub, + email: userPayload.email, + firstName: userPayload.given_name, + lastName: userPayload.family_name, + groups: userPayload.groups || [], + }; } export function bypassFn(request: FastifyRequest) { - try { - return !!request.headers[tokenHeaderName] - } catch (_e) {} - return false + try { + return !!request.headers[tokenHeaderName]; + } catch (_e) {} + return false; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts index 1d0159d40..a0da3e46b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts @@ -1,42 +1,47 @@ -import { serviceContract, swaggerUiPath, systemContract } from '@cpn-console/shared' -import type { KeycloakOptions } from 'fastify-keycloak-adapter' import { - keycloakClientId, - keycloakClientSecret, - keycloakDomain, - keycloakProtocol, - keycloakRealm, - keycloakRedirectUri, - sessionSecret, -} from './env.js' -import { bypassFn, userPayloadMapper } from './keycloak-utils.js' + serviceContract, + swaggerUiPath, + systemContract, +} from '@cpn-console/shared'; +import type { KeycloakOptions } from 'fastify-keycloak-adapter'; + +import { + keycloakClientId, + keycloakClientSecret, + keycloakDomain, + keycloakProtocol, + keycloakRealm, + keycloakRedirectUri, + sessionSecret, +} from './env.js'; +import { bypassFn, userPayloadMapper } from './keycloak-utils.js'; export const keycloakConf = { - appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', - keycloakSubdomain: `${keycloakDomain}/realms/${keycloakRealm}`, - clientId: keycloakClientId ?? '', - clientSecret: keycloakClientSecret ?? '', - useHttps: keycloakProtocol === 'https', - disableCookiePlugin: true, - disableSessionPlugin: true, - // @ts-ignore - userPayloadMapper, - retries: 5, - excludedPatterns: [ - systemContract.getVersion.path, - systemContract.getHealth.path, - serviceContract.getServiceHealth.path, - `${swaggerUiPath}/**`, - ], - bypassFn, -} as const satisfies KeycloakOptions + appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', + keycloakSubdomain: `${keycloakDomain}/realms/${keycloakRealm}`, + clientId: keycloakClientId ?? '', + clientSecret: keycloakClientSecret ?? '', + useHttps: keycloakProtocol === 'https', + disableCookiePlugin: true, + disableSessionPlugin: true, + // @ts-ignore + userPayloadMapper, + retries: 5, + excludedPatterns: [ + systemContract.getVersion.path, + systemContract.getHealth.path, + serviceContract.getServiceHealth.path, + `${swaggerUiPath}/**`, + ], + bypassFn, +} as const satisfies KeycloakOptions; export const sessionConf = { - cookieName: 'sessionId', - secret: sessionSecret || 'a-very-strong-secret-with-more-than-32-char', - cookie: { - httpOnly: true, - secure: true, - }, - expires: 1_800_000, -} + cookieName: 'sessionId', + secret: sessionSecret || 'a-very-strong-secret-with-more-than-32-char', + cookie: { + httpOnly: true, + secure: true, + }, + expires: 1_800_000, +}; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts index 4483c8398..cdadb61a0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts @@ -1,97 +1,115 @@ -import type { FastifyBaseLogger, FastifyLogFn, PinoLoggerOptions } from 'fastify/types/logger.js' -import type { XOR } from '@cpn-console/shared' -import { logger as customLogger } from '@old-server/app.js' +import type { XOR } from '@cpn-console/shared'; +import { logger as customLogger } from '@old-server/app.js'; +import type { + FastifyBaseLogger, + FastifyLogFn, + PinoLoggerOptions, +} from 'fastify/types/logger.js'; export const customLevels = { - audit: 25, -} + audit: 25, +}; export const loggerConf: Record = { - development: { - transport: { - target: 'pino-pretty', - options: { - translateTime: 'dd/mm/yyyy - HH:MM:ss Z', - ignore: 'pid,hostname', - colorize: true, - singleLine: true, - }, + development: { + transport: { + target: 'pino-pretty', + options: { + translateTime: 'dd/mm/yyyy - HH:MM:ss Z', + ignore: 'pid,hostname', + colorize: true, + singleLine: true, + }, + }, + customLevels, + level: process.env.LOG_LEVEL ?? 'debug', }, - customLevels, - level: process.env.LOG_LEVEL ?? 'debug', - }, - production: { - customLevels, - level: process.env.LOG_LEVEL ?? 'audit', - }, - test: { - level: 'silent', - }, -} + production: { + customLevels, + level: process.env.LOG_LEVEL ?? 'audit', + }, + test: { + level: 'silent', + }, +}; -type LoggerType = 'info' | 'warn' | 'error' | 'fatal' | 'trace' | 'debug' | 'audit' | undefined +type LoggerType = + | 'info' + | 'warn' + | 'error' + | 'fatal' + | 'trace' + | 'debug' + | 'audit' + | undefined; const loggerWrapper = { - level: '', - child: () => loggerWrapper, - silent: () => {}, - audit: (msg: string | unknown) => console.log(msg), - info: (msg: string | unknown) => console.log(msg), - warn: (msg: string | unknown) => console.warn(msg), - error: (msg: string | unknown) => console.error(msg), - fatal: (msg: string | unknown) => console.error(msg), - trace: (msg: string | unknown) => console.trace(msg), - debug: (msg: string | unknown) => console.debug(msg), -} + level: '', + child: () => loggerWrapper, + silent: () => {}, + audit: (msg: string | unknown) => console.log(msg), + info: (msg: string | unknown) => console.log(msg), + warn: (msg: string | unknown) => console.warn(msg), + error: (msg: string | unknown) => console.error(msg), + fatal: (msg: string | unknown) => console.error(msg), + trace: (msg: string | unknown) => console.trace(msg), + debug: (msg: string | unknown) => console.debug(msg), +}; export function log( - type: LoggerType, - { - reqId, - userId, - tokenId, - message, - error, - infos, - }: { - reqId?: string - userId?: string - tokenId?: string - infos?: Record - } & XOR<{ message: string }, { error: Record | string | Error }>, + type: LoggerType, + { + reqId, + userId, + tokenId, + message, + error, + infos, + }: { + reqId?: string; + userId?: string; + tokenId?: string; + infos?: Record; + } & XOR< + { message: string }, + { error: Record | string | Error } + >, ) { - const logger = customLogger || loggerWrapper + const logger = customLogger || loggerWrapper; - const logInfos = { - message, - infos, - reqId, - userId, - tokenId, - } + const logInfos = { + message, + infos, + reqId, + userId, + tokenId, + }; - if (error) { - const errorInfos = { - ...logInfos, - error: { - message: typeof error === 'string' ? error : error?.message || 'unexpected error', - trace: error instanceof Error && error?.stack, - }, + if (error) { + const errorInfos = { + ...logInfos, + error: { + message: + typeof error === 'string' + ? error + : error?.message || 'unexpected error', + trace: error instanceof Error && error?.stack, + }, + }; + logger.error({ ...errorInfos }); + return; } - logger.error({ ...errorInfos }) - return - } - logger[type || 'info']({ reqId, userId, logInfos }) + logger[type || 'info']({ reqId, userId, logInfos }); } export interface CustomLogger extends FastifyBaseLogger { - /** - * Log at `'audit'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. - * If more args follows `msg`, these will be used to format `msg` using `util.format`. - * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string - */ - audit: FastifyLogFn + /** + * Log at `'audit'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. + * If more args follows `msg`, these will be used to format `msg` using `util.format`. + * + * @typeParam T: the interface of the object being serialized. Default is object. + * @param obj: object to be serialized + * @param msg: the log message to write + * @param ...args: format string values when `msg` is a format string + */ + audit: FastifyLogFn; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts index 90f04ecbc..93c86f7bb 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts @@ -1,152 +1,190 @@ -import fp from 'fastify-plugin' -import type { Repository } from '@prisma/client' -import type { PluginsManifests, RepoCreds, ServiceInfos } from '@cpn-console/hooks' -import { editStrippers, populatePluginManifests } from '@cpn-console/hooks' -import { DEFAULT, DISABLED, PROJECT_PERMS } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import type { UserDetails } from '../types/index.js' -import type * as utilsController from '../utils/controller.js' +import type { + PluginsManifests, + RepoCreds, + ServiceInfos, +} from '@cpn-console/hooks'; +import { editStrippers, populatePluginManifests } from '@cpn-console/hooks'; +import { DEFAULT, DISABLED, PROJECT_PERMS } from '@cpn-console/shared'; +import { faker } from '@faker-js/faker'; +import type { Repository } from '@prisma/client'; +import fp from 'fastify-plugin'; -let requestor: Requestor +import type { UserDetails } from '../types/index.js'; +import type * as utilsController from '../utils/controller.js'; + +let requestor: Requestor; export function setRequestor(user: Requestor = getRandomRequestor()) { - requestor = user + requestor = user; } export function getRequestor() { - return requestor + return requestor; } export async function mockSessionPlugin() { - const sessionPlugin = (app, opt, next) => { - app.addHook('onRequest', (req, res, next) => { - req.session = { user: getRequestor() } - next() - }) - next() - } + const sessionPlugin = (app, opt, next) => { + app.addHook('onRequest', (req, res, next) => { + req.session = { user: getRequestor() }; + next(); + }); + next(); + }; - return { default: fp(sessionPlugin) } + return { default: fp(sessionPlugin) }; } export async function mockHooksPackage() { - const hookTemplate = { - execute: () => ({ - args: {}, - failed: false, - }), - validate: () => ({ - failed: false, - }), - } - - return { - editStrippers, - populatePluginManifests, - services: { - getStatus: () => [], - refreshStatus: async () => [], - }, - PluginApi: class { }, - servicesInfos: { - registry: { title: 'Harbor', name: 'registry', to: () => 'test' }, - plugin2: { title: 'Plugin2', name: 'plugin2', to: () => ({ to: 'test', title: 'Test' }) }, - plugin3: { title: 'Plugin3', name: 'plugin3', to: () => [{ to: 'test', title: 'Test' }] }, - plugin4: { title: 'Plugin4', name: 'plugin4', to: () => [{ to: 'test' }] }, - plugin5: { title: 'Plugin5', name: 'plugin5' }, - } as Record, - pluginsManifests: { - registry: { - title: 'Harbor', - global: [{ - kind: 'switch', - initialValue: DEFAULT, - key: 'test2', - permissions: { - admin: { read: true, write: true }, - user: { read: true, write: false }, - }, - title: 'Test2', - value: DEFAULT, - description: 'description', - }], - project: [{ - kind: 'switch', - key: 'test2', - permissions: { - admin: { read: true, write: true }, - user: { read: true, write: true }, - }, - title: 'Test', - value: DEFAULT, - initialValue: DISABLED, - }], - }, - } as PluginsManifests, - hooks: { - // projects - getProjectSecrets: { + const hookTemplate = { execute: () => ({ - failed: false, - args: {}, - results: { + args: {}, + failed: false, + }), + validate: () => ({ + failed: false, + }), + }; + + return { + editStrippers, + populatePluginManifests, + services: { + getStatus: () => [], + refreshStatus: async () => [], + }, + PluginApi: class {}, + servicesInfos: { + registry: { title: 'Harbor', name: 'registry', to: () => 'test' }, + plugin2: { + title: 'Plugin2', + name: 'plugin2', + to: () => ({ to: 'test', title: 'Test' }), + }, + plugin3: { + title: 'Plugin3', + name: 'plugin3', + to: () => [{ to: 'test', title: 'Test' }], + }, + plugin4: { + title: 'Plugin4', + name: 'plugin4', + to: () => [{ to: 'test' }], + }, + plugin5: { title: 'Plugin5', name: 'plugin5' }, + } as Record, + pluginsManifests: { registry: { - secrets: { - token: 'myToken', - }, - status: { - failed: false, - }, + title: 'Harbor', + global: [ + { + kind: 'switch', + initialValue: DEFAULT, + key: 'test2', + permissions: { + admin: { read: true, write: true }, + user: { read: true, write: false }, + }, + title: 'Test2', + value: DEFAULT, + description: 'description', + }, + ], + project: [ + { + kind: 'switch', + key: 'test2', + permissions: { + admin: { read: true, write: true }, + user: { read: true, write: true }, + }, + title: 'Test', + value: DEFAULT, + initialValue: DISABLED, + }, + ], }, - }, - }), - }, - upsertProject: hookTemplate, - deleteProject: hookTemplate, - // clusters - upsertCluster: hookTemplate, - deleteCluster: hookTemplate, - // user - retrieveUserByEmail: hookTemplate, - }, - } + } as PluginsManifests, + hooks: { + // projects + getProjectSecrets: { + execute: () => ({ + failed: false, + args: {}, + results: { + registry: { + secrets: { + token: 'myToken', + }, + status: { + failed: false, + }, + }, + }, + }), + }, + upsertProject: hookTemplate, + deleteProject: hookTemplate, + // clusters + upsertCluster: hookTemplate, + deleteCluster: hookTemplate, + // user + retrieveUserByEmail: hookTemplate, + }, + }; } -export type ReposCreds = Record +export type ReposCreds = Record; -type Requestor = Partial +type Requestor = Partial; export function getRandomRequestor(user?: Requestor): Partial { - return { - id: user?.id ?? faker.string.uuid(), - email: user?.email ?? faker.internet.email(), - firstName: user?.firstName ?? faker.person.firstName(), - lastName: user?.lastName ?? faker.person.lastName(), - type: 'human', - ...user?.groups !== null && { groups: user?.groups ?? [] }, - } + return { + id: user?.id ?? faker.string.uuid(), + email: user?.email ?? faker.internet.email(), + firstName: user?.firstName ?? faker.person.firstName(), + lastName: user?.lastName ?? faker.person.lastName(), + type: 'human', + ...(user?.groups !== null && { groups: user?.groups ?? [] }), + }; } -export function getUserMockInfos(isAdmin: boolean, user?: UserDetails): utilsController.UserProfile & utilsController.ProjectPermState -export function getUserMockInfos(isAdmin: boolean, user?: UserDetails, project?: utilsController.ProjectPermState): utilsController.UserProjectProfile & utilsController.ProjectPermState -export function getUserMockInfos(isAdmin: boolean, user = getRandomRequestor(), project?: utilsController.ProjectPermState): utilsController.UserProfile | utilsController.UserProjectProfile { - return { - adminPermissions: isAdmin ? 2n : 0n, - user, - ...project, - } +export function getUserMockInfos( + isAdmin: boolean, + user?: UserDetails, +): utilsController.UserProfile & utilsController.ProjectPermState; +export function getUserMockInfos( + isAdmin: boolean, + user?: UserDetails, + project?: utilsController.ProjectPermState, +): utilsController.UserProjectProfile & utilsController.ProjectPermState; +export function getUserMockInfos( + isAdmin: boolean, + user = getRandomRequestor(), + project?: utilsController.ProjectPermState, +): utilsController.UserProfile | utilsController.UserProjectProfile { + return { + adminPermissions: isAdmin ? 2n : 0n, + user: user as unknown as UserDetails, + ...project, + }; } -export function getProjectMockInfos({ projectId, projectLocked, projectOwnerId, projectPermissions, projectStatus }: Partial): utilsController.ProjectPermState { - return { - projectId: projectId ?? faker.string.uuid(), - projectLocked: projectLocked ?? false, - projectOwnerId: projectOwnerId ?? faker.string.uuid(), - projectStatus: projectStatus ?? 'created', - projectPermissions: projectPermissions ?? PROJECT_PERMS.MANAGE, - } +export function getProjectMockInfos({ + projectId, + projectLocked, + projectOwnerId, + projectPermissions, + projectStatus, +}: Partial): utilsController.ProjectPermState { + return { + projectId: projectId ?? faker.string.uuid(), + projectLocked: projectLocked ?? false, + projectOwnerId: projectOwnerId ?? faker.string.uuid(), + projectStatus: projectStatus ?? 'created', + projectPermissions: projectPermissions ?? PROJECT_PERMS.MANAGE, + }; } export const atDates = { - createdAt: new Date(), - updatedAt: new Date(), -} + createdAt: new Date(), + updatedAt: new Date(), +}; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts index 987667776..79fc98b7f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts @@ -1,9 +1,10 @@ -import type { PluginManagerOptions } from '@cpn-console/hooks' -import { isCI, isInt, isProd } from './env.js' +import type { PluginManagerOptions } from '@cpn-console/hooks'; + +import { isCI, isInt, isProd } from './env.js'; export const pluginManagerOptions: PluginManagerOptions = { - mockHooks: isCI || (!isProd && !isInt), - mockMonitoring: isCI || (!isProd && !isInt), - mockExternalServices: isCI || (!isProd && !isInt), - startPlugins: (!isCI && isProd) || isInt, -} + mockHooks: isCI || (!isProd && !isInt), + mockMonitoring: isCI || (!isProd && !isInt), + mockExternalServices: isCI || (!isProd && !isInt), + startPlugins: (!isCI && isProd) || isInt, +}; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts index a5e0cd9f4..30a351735 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts @@ -1,157 +1,163 @@ -import { describe, expect, it } from 'vitest' -import { genericProxy } from './proxy.js' +import { describe, expect, it } from 'vitest'; + +import { genericProxy } from './proxy.js'; // Création d'une cible de test const target = { - async fetchData(id: string) { - return { id, data: 'Mocked data' } - }, - async otherMethod(id: string) { - return { id, data: 'Mocked data' } - }, -} + async fetchData(id: string) { + return { id, data: 'Mocked data' }; + }, + async otherMethod(id: string) { + return { id, data: 'Mocked data' }; + }, +}; describe('test calls without ID passed', () => { - // Test d'appel de méthode sans ID - it('calling method without ID', async () => { - const proxied = genericProxy(target) - const result = await proxied.fetchData() - expect(result).toEqual({ id: undefined, data: 'Mocked data' }) - }) - - // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie - it('test when no ID is provided', async () => { - // Création d'une cible de test - const target = { - async fetchData() { - return 'No ID provided' - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - // Appel à la méthode fetchData sans ID - const result = await proxied.fetchData() - - // Vérification que le résultat est correct - expect(result).toBe('No ID provided') - }) - - // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie avec une promesse en cours - it('test when no ID is provided with pending promise', async () => { - // Création d'une cible de test - const target = { - async fetchData() { - return new Promise(resolve => setTimeout(() => resolve('Pending result'), 100)) - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - // Appel à la méthode fetchData sans ID - const promise1 = proxied.fetchData() - const promise2 = proxied.fetchData() // Deuxième appel avant la résolution du premier - - // Attendre que la première promesse se résolve - const result1 = await promise1 - - // Vérification que le résultat de la première promesse est correct - expect(result1).toBe('Pending result') - - // Attendre que la deuxième promesse se résolve - const result2 = await promise2 - - // Vérification que le résultat de la deuxième promesse est correct - expect(result2).toBe('Pending result') - }) - // Test pour vérifier que l'erreur est levée lorsque args est fourni sans ID - it('test error when args provided without ID', async () => { - // Création d'une cible de test - const target = { - async fetchData(_id: string, _args: any) { - return 'No ID provided' - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - const args = { key: 'value' } - - // Appel de la fonction fetchData avec des arguments mais sans ID - await expect(proxied.fetchData(undefined, args)).rejects.toThrow('ID is required when args are provided') - }) -}) + // Test d'appel de méthode sans ID + it('calling method without ID', async () => { + const proxied = genericProxy(target); + const result = await proxied.fetchData(); + expect(result).toEqual({ id: undefined, data: 'Mocked data' }); + }); + + // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie + it('test when no ID is provided', async () => { + // Création d'une cible de test + const target = { + async fetchData() { + return 'No ID provided'; + }, + }; + + // Création du proxy + const proxied = genericProxy(target); + + // Appel à la méthode fetchData sans ID + const result = await proxied.fetchData(); + + // Vérification que le résultat est correct + expect(result).toBe('No ID provided'); + }); + + // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie avec une promesse en cours + it('test when no ID is provided with pending promise', async () => { + // Création d'une cible de test + const target = { + async fetchData() { + return new Promise((resolve) => + setTimeout(() => resolve('Pending result'), 100), + ); + }, + }; + + // Création du proxy + const proxied = genericProxy(target); + + // Appel à la méthode fetchData sans ID + const promise1 = proxied.fetchData(); + const promise2 = proxied.fetchData(); // Deuxième appel avant la résolution du premier + + // Attendre que la première promesse se résolve + const result1 = await promise1; + + // Vérification que le résultat de la première promesse est correct + expect(result1).toBe('Pending result'); + + // Attendre que la deuxième promesse se résolve + const result2 = await promise2; + + // Vérification que le résultat de la deuxième promesse est correct + expect(result2).toBe('Pending result'); + }); + // Test pour vérifier que l'erreur est levée lorsque args est fourni sans ID + it('test error when args provided without ID', async () => { + // Création d'une cible de test + const target = { + async fetchData(_id: string, _args: any) { + return 'No ID provided'; + }, + }; + + // Création du proxy + const proxied = genericProxy(target); + + const args = { key: 'value' }; + + // Appel de la fonction fetchData avec des arguments mais sans ID + await expect(proxied.fetchData(undefined, args)).rejects.toThrow( + 'ID is required when args are provided', + ); + }); +}); describe('test calls with ID passed', () => { - // Test d'appel de méthode avec ID - it('calling method with ID', async () => { - const proxied = genericProxy(target) - const result = await proxied.fetchData('123') - expect(result).toEqual({ id: '123', data: 'Mocked data' }) - }) - - // Test d'appel de méthode avec exclusion en cours - it('calling method with exclusion in progress', async () => { - const proxied = genericProxy(target, { fetchData: ['otherMethod'] }) - // Simuler une exécution en cours pour la méthode exclue - proxied.otherMethod('456') - - // Maintenant, tenter d'appeler fetchData pour le même ID devrait échouer - await expect(proxied.fetchData('456')).rejects.toThrow( - 'otherMethod in progress on 456, can\'t fetchData', - ) - }) - - // Fonction de test asynchrone pour tester le mélange des nextArgs - it('test mixing nextArgs from concurrent promises', async () => { - // Création d'une cible de test - const target = { - async fetchData(id: string, args?: object) { - return { id, args } - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - const promise1 = proxied.fetchData('123', { key1: 'value1' }) - // Appels successifs à fetchData avec différents arguments - const promise2 = proxied.fetchData('123', { key2: 'value2' }) - - // Promesse concurrente avec des nextArgs différents - const promise3 = proxied.fetchData('123', { key3: 'value3' }) - - // Attendre que les promesses se résolvent - const result1 = await promise1 - const result2 = await promise2 - const result3 = await promise3 - - // Vérification que les nextArgs de promise2 et promise3 ont été correctement mélangés - expect(result1.args).toEqual({ key1: 'value1' }) - expect(result2.args).toEqual({ key2: 'value2', key3: 'value3' }) - expect(result3.args).toEqual({ key2: 'value2', key3: 'value3' }) - }) - - it('test rejection of set attempt', () => { - // Création d'une cible de test - const target = { - async fetchData() { - return 'Mocked data' - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - // Tentative de définir une nouvelle propriété sur le proxy - const setAttempt = () => { - proxied.fetchData = () => new Promise(resolve => resolve('illegal')) - } - - // Vérification que la tentative de set est rejetée - expect(setAttempt).toThrow(TypeError) - }) -}) + // Test d'appel de méthode avec ID + it('calling method with ID', async () => { + const proxied = genericProxy(target); + const result = await proxied.fetchData('123'); + expect(result).toEqual({ id: '123', data: 'Mocked data' }); + }); + + // Test d'appel de méthode avec exclusion en cours + it('calling method with exclusion in progress', async () => { + const proxied = genericProxy(target, { fetchData: ['otherMethod'] }); + // Simuler une exécution en cours pour la méthode exclue + proxied.otherMethod('456'); + + // Maintenant, tenter d'appeler fetchData pour le même ID devrait échouer + await expect(proxied.fetchData('456')).rejects.toThrow( + "otherMethod in progress on 456, can't fetchData", + ); + }); + + // Fonction de test asynchrone pour tester le mélange des nextArgs + it('test mixing nextArgs from concurrent promises', async () => { + // Création d'une cible de test + const target = { + async fetchData(id: string, args?: object) { + return { id, args }; + }, + }; + + // Création du proxy + const proxied = genericProxy(target); + + const promise1 = proxied.fetchData('123', { key1: 'value1' }); + // Appels successifs à fetchData avec différents arguments + const promise2 = proxied.fetchData('123', { key2: 'value2' }); + + // Promesse concurrente avec des nextArgs différents + const promise3 = proxied.fetchData('123', { key3: 'value3' }); + + // Attendre que les promesses se résolvent + const result1 = await promise1; + const result2 = await promise2; + const result3 = await promise3; + + // Vérification que les nextArgs de promise2 et promise3 ont été correctement mélangés + expect(result1.args).toEqual({ key1: 'value1' }); + expect(result2.args).toEqual({ key2: 'value2', key3: 'value3' }); + expect(result3.args).toEqual({ key2: 'value2', key3: 'value3' }); + }); + + it('test rejection of set attempt', () => { + // Création d'une cible de test + const target = { + async fetchData() { + return 'Mocked data'; + }, + }; + + // Création du proxy + const proxied = genericProxy(target); + + // Tentative de définir une nouvelle propriété sur le proxy + const setAttempt = () => { + proxied.fetchData = () => + new Promise((resolve) => resolve('illegal')); + }; + + // Vérification que la tentative de set est rejetée + expect(setAttempt).toThrow(TypeError); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts index ef915a7d1..f300e3a59 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts @@ -1,78 +1,99 @@ // @ts-nocheck un enfer à typer, pour plus tard -type Tracker> = Record - nextArgs?: [string] -}>> | Promise +type Tracker> = + | Record< + keyof T, + Record< + string, + { + currentExec?: Promise; + nextArgs?: [string]; + } + > + > + | Promise; -type Target = Record Promise> -type Excludes = Partial>> | undefined -const toTarget = (target: T) => ({ tracker: {} as Tracker, methods: target }) +type Target = Record Promise>; +type Excludes = + | Partial>> + | undefined; +const toTarget = (target: T) => ({ + tracker: {} as Tracker, + methods: target, +}); // @ts-ignore export function genericProxy(proxied: T, excludes: Excludes = {}): T { - return new Proxy(toTarget(proxied), { - get({ methods, tracker }, property: string) { - if (!(property in methods)) return - return async (...args) => { - const id = args[0] as string + return new Proxy(toTarget(proxied), { + get({ methods, tracker }, property: string) { + if (!(property in methods)) return; + return async (...args) => { + const id = args[0] as string; - if (!id && args.length > 0) { - throw new Error('ID is required when args are provided') - } + if (!id && args.length > 0) { + throw new Error('ID is required when args are provided'); + } - if (!id) { - if (tracker[property] instanceof Promise) { - return tracker[property] - } - const p = methods[property]() - if (p instanceof Promise) { - tracker[property] = p - p.then(() => { - delete tracker[property] - }) - } - return p - } - if (!tracker[property]) { - tracker[property] = {} - } + if (!id) { + if (tracker[property] instanceof Promise) { + return tracker[property]; + } + const p = methods[property](); + if (p instanceof Promise) { + tracker[property] = p; + p.then(() => { + delete tracker[property]; + }); + } + return p; + } + if (!tracker[property]) { + tracker[property] = {}; + } - for (const testExclude of excludes[property] ?? []) { - // @ts-ignore - if (tracker?.[testExclude]?.[id]?.currentExec) { - throw new Error(`${String(testExclude)} in progress on ${id}, can't ${String(property)}`) - } - } + for (const testExclude of excludes[property] ?? []) { + // @ts-ignore + if (tracker?.[testExclude]?.[id]?.currentExec) { + throw new Error( + `${String(testExclude)} in progress on ${id}, can't ${String(property)}`, + ); + } + } - if (id in tracker[property]) { - if (args[1]) { - tracker[property][id].nextArgs = { - ...(tracker[property][id].nextArgs ?? {}), - ...args[1], - } - } - if (tracker[property][id].currentExec) { - return new Promise((resolve) => { - tracker[property][id].currentExec.then(() => { - resolve(tracker[property][id].currentExec ?? methods[property](id, tracker[property][id].nextArgs)) - }) - }) - } - } - const p = methods[property](...args) - tracker[property][id] = { - currentExec: p, - nextArgs: undefined, - } - tracker[property][id].currentExec = p - p.then(() => { - tracker[property][id].currentExec = undefined - }) - return p - } - }, - set() { - return false - }, - }) as T + if (id in tracker[property]) { + if (args[1]) { + tracker[property][id].nextArgs = { + ...(tracker[property][id].nextArgs ?? {}), + ...args[1], + }; + } + if (tracker[property][id].currentExec) { + return new Promise((resolve) => { + tracker[property][id].currentExec.then(() => { + resolve( + tracker[property][id].currentExec ?? + methods[property]( + id, + tracker[property][id].nextArgs, + ), + ); + }); + }); + } + } + const p = methods[property](...args); + tracker[property][id] = { + currentExec: p, + nextArgs: undefined, + }; + tracker[property][id].currentExec = p; + p.then(() => { + tracker[property][id].currentExec = undefined; + }); + return p; + }; + }, + set() { + return false; + }, + }) as T; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts index 49580d2b5..9dffdd343 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts @@ -1,47 +1,51 @@ -import { describe, expect, it } from 'vitest' -import { exclude } from '@cpn-console/shared' -import { filterObjectByKeys } from './queries-tools.js' +import { exclude } from '@cpn-console/shared'; +import { describe, expect, it } from 'vitest'; + +import { filterObjectByKeys } from './queries-tools.js'; describe('queries-tools', () => { - it('should return a filtered object (filterObjectByKeys)', () => { - const initial = { - id: 'thisIsAnId', - name: 'alsoKeepThisKey', - description: 'keepThisKey', - } - const desired = { - name: 'alsoKeepThisKey', - description: 'keepThisKey', - } + it('should return a filtered object (filterObjectByKeys)', () => { + const initial = { + id: 'thisIsAnId', + name: 'alsoKeepThisKey', + description: 'keepThisKey', + }; + const desired = { + name: 'alsoKeepThisKey', + description: 'keepThisKey', + }; - const transformed = filterObjectByKeys(initial, ['name', 'description']) + const transformed = filterObjectByKeys(initial, [ + 'name', + 'description', + ]); - expect(transformed).toMatchObject(desired) - }) + expect(transformed).toMatchObject(desired); + }); - it('should return a filtered object (exclude)', () => { - const initial = { - id: 'thisIsAnId', - name: 'myProjectName', - environment: { - permissions: { - password: 'secret', - id: 'notSecret', - }, - }, - } - const desired = { - id: 'thisIsAnId', - name: 'myProjectName', - environment: { - permissions: { - id: 'notSecret', - }, - }, - } + it('should return a filtered object (exclude)', () => { + const initial = { + id: 'thisIsAnId', + name: 'myProjectName', + environment: { + permissions: { + password: 'secret', + id: 'notSecret', + }, + }, + }; + const desired = { + id: 'thisIsAnId', + name: 'myProjectName', + environment: { + permissions: { + id: 'notSecret', + }, + }, + }; - const transformed = exclude(initial, ['password']) + const transformed = exclude(initial, ['password']); - expect(transformed).toMatchObject(desired) - }) -}) + expect(transformed).toMatchObject(desired); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts index 856ca277f..fb9fc0bb6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts @@ -1,11 +1,12 @@ -export const dbKeysExcluded = ['updatedAt', 'createdAt'] +export const dbKeysExcluded = ['updatedAt', 'createdAt']; // TODO // @ts-ignore supprimer cette fonction et utiliser des schémas zod où elle est utilisé export function filterObjectByKeys(obj, keys) { - return Object.fromEntries( - Object.entries(obj)?.filter(([key, _value]) => keys.includes(key)), - ) + return Object.fromEntries( + Object.entries(obj)?.filter(([key, _value]) => keys.includes(key)), + ); } -export const uuid: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i +export const uuid: RegExp = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts index f754da343..3706edf64 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts @@ -1,148 +1,154 @@ -import { describe, expect, it } from 'vitest' -import { createRandomDbSetup } from '@cpn-console/test-utils' +import { createRandomDbSetup } from '@cpn-console/test-utils'; +import { describe, expect, it } from 'vitest'; describe('random utils', () => { - // TODO - it.skip('should create a random db for tests', () => { - const db = createRandomDbSetup({ nbUsers: 3, nbRepo: 1, envs: ['dev', 'prod'] }) - expect(db).toEqual( - expect.objectContaining({ - stages: expect.arrayContaining([ - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - ]), - quotas: expect.arrayContaining([ - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - ]), - project: expect.objectContaining({ - id: expect.any(String), - name: expect.any(String), - clusters: expect.arrayContaining([{ - caData: expect.any(String), - server: expect.any(String), - tlsServername: expect.any(String), - }]), - status: expect.any(String), - locked: expect.any(Boolean), - roles: expect.arrayContaining([ - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - ]), - repositories: expect.any(Array), - environments: expect.arrayContaining([ - { - id: expect.any(String), - stageId: expect.any(String), - projectId: expect.any(String), - quotaId: expect.any(String), - status: expect.any(String), - permissions: expect.any(Array), - clusters: expect.any(Array), - }, - { - id: expect.any(String), - stageId: expect.any(String), - projectId: expect.any(String), - quotaId: expect.any(String), - status: expect.any(String), - permissions: expect.any(Array), - clusters: expect.any(Array), - }, - ]), - }), - users: expect.arrayContaining([ - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - ]), - }), - ) - }) -}) + // TODO + it.skip('should create a random db for tests', () => { + const db = createRandomDbSetup({ + nbUsers: 3, + nbRepo: 1, + envs: ['dev', 'prod'], + }); + expect(db).toEqual( + expect.objectContaining({ + stages: expect.arrayContaining([ + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + ]), + quotas: expect.arrayContaining([ + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + ]), + project: expect.objectContaining({ + id: expect.any(String), + name: expect.any(String), + clusters: expect.arrayContaining([ + { + caData: expect.any(String), + server: expect.any(String), + tlsServername: expect.any(String), + }, + ]), + status: expect.any(String), + locked: expect.any(Boolean), + roles: expect.arrayContaining([ + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + ]), + repositories: expect.any(Array), + environments: expect.arrayContaining([ + { + id: expect.any(String), + stageId: expect.any(String), + projectId: expect.any(String), + quotaId: expect.any(String), + status: expect.any(String), + permissions: expect.any(Array), + clusters: expect.any(Array), + }, + { + id: expect.any(String), + stageId: expect.any(String), + projectId: expect.any(String), + quotaId: expect.any(String), + status: expect.any(String), + permissions: expect.any(Array), + clusters: expect.any(Array), + }, + ]), + }), + users: expect.arrayContaining([ + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + ]), + }), + ); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts index e34cc8f52..d30d9a227 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts @@ -1,18 +1,15 @@ /// -import { URL, fileURLToPath } from 'node:url' -import { defineConfig } from 'vite' +import path from 'path'; +import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [ - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), + plugins: [], + resolve: { + alias: { + '@': path.join(__dirname, '..', '/src'), + }, }, - }, - test: { - poolMatchGlobs: [ - ['**/resources/**/*.spec.ts', 'forks'], - ], - }, -}) + test: { + poolMatchGlobs: [['**/resources/**/*.spec.ts', 'forks']], + }, +} as any); diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts index 596420f7d..6dd4100e5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts @@ -1,11 +1,11 @@ -process.env.ARGOCD_URL = 'https://argo-cd.readthedocs.io' -process.env.GITLAB_URL = 'https://gitlab.com' -process.env.HARBOR_URL = 'https://goharbor.io' -process.env.NEXUS_URL = 'https://sonatype.com/products/nexus-repository' -process.env.SONARQUBE_URL = 'https://www.sonarqube.org' -process.env.VAULT_URL = 'https://www.vaultproject.io' -process.env.PROJECTS_ROOT_DIR = 'forge-mi/projects' -process.env.KEYCLOAK_REDIRECT_URI = 'http://console.dso.local' -process.env.CONTACT_EMAIL = 'cloudpinative-relations@interieur.gouv.fr' -process.env.OPENCDS_URL = 'https://opencds.gouv.fr' -process.env.OPENCDS_API_TOKEN = 'test_token' +process.env.ARGOCD_URL = 'https://argo-cd.readthedocs.io'; +process.env.GITLAB_URL = 'https://gitlab.com'; +process.env.HARBOR_URL = 'https://goharbor.io'; +process.env.NEXUS_URL = 'https://sonatype.com/products/nexus-repository'; +process.env.SONARQUBE_URL = 'https://www.sonarqube.org'; +process.env.VAULT_URL = 'https://www.vaultproject.io'; +process.env.PROJECTS_ROOT_DIR = 'forge-mi/projects'; +process.env.KEYCLOAK_REDIRECT_URI = 'http://console.dso.local'; +process.env.CONTACT_EMAIL = 'cloudpinative-relations@interieur.gouv.fr'; +process.env.OPENCDS_URL = 'https://opencds.gouv.fr'; +process.env.OPENCDS_API_TOKEN = 'test_token'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts index deb1bfe64..93ffd45d4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts @@ -1,34 +1,34 @@ -import { fileURLToPath } from 'node:url' -import { mergeConfig } from 'vite' -import { configDefaults, defineConfig } from 'vitest/config' -import viteConfig from './vite.config' +import { mergeConfig } from 'vite'; +import { configDefaults, defineConfig } from 'vitest/config'; + +import viteConfig from './vite.config'; export default mergeConfig( - viteConfig, - defineConfig({ - test: { - reporters: ['default', 'hanging-process'], - environment: 'node', - testTimeout: 2000, - coverage: { - provider: 'v8', - reporter: ['text', 'lcov'], - include: ['src/**'], - exclude: [ - '**/types', - '**/mocks', - '**/*.spec.ts', - '**/*.d.ts', - '**/*.vue', - '**/queries.ts', - '**/mocks.ts', - ], - }, - include: ['src/**/*.spec.{ts,js}'], - exclude: [...configDefaults.exclude, 'e2e/*'], - setupFiles: ['./vitest-init.ts'], - root: fileURLToPath(new URL('./', import.meta.url)), - pool: 'forks', - }, - }), -) + viteConfig as any, + defineConfig({ + test: { + reporters: ['default', 'hanging-process'], + environment: 'node', + testTimeout: 2000, + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**'], + exclude: [ + '**/types', + '**/mocks', + '**/*.spec.ts', + '**/*.d.ts', + '**/*.vue', + '**/queries.ts', + '**/mocks.ts', + ], + }, + include: ['src/**/*.spec.{ts,js}'], + exclude: [...configDefaults.exclude, 'e2e/*'], + setupFiles: ['./vitest-init.ts'], + root: __dirname, + pool: 'forks', + }, + }), +); diff --git a/apps/server-nestjs/src/main.ts b/apps/server-nestjs/src/main.ts index e02a6e353..23fc8c0fd 100644 --- a/apps/server-nestjs/src/main.ts +++ b/apps/server-nestjs/src/main.ts @@ -1,8 +1,9 @@ import { NestFactory } from '@nestjs/core'; + import { AppModule } from './app.module'; async function bootstrap() { - const app = await NestFactory.create(AppModule); - await app.listen(process.env.PORT ?? 8080); + const app = await NestFactory.create(AppModule); + await app.listen(process.env.PORT ?? 8080); } bootstrap(); diff --git a/apps/server-nestjs/test/app.e2e-spec.ts b/apps/server-nestjs/test/app.e2e-spec.ts index 36852c54f..cb271d715 100644 --- a/apps/server-nestjs/test/app.e2e-spec.ts +++ b/apps/server-nestjs/test/app.e2e-spec.ts @@ -1,25 +1,26 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; import { App } from 'supertest/types'; + import { AppModule } from './../src/app.module'; describe('AppController (e2e)', () => { - let app: INestApplication; + let app: INestApplication; - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); - app = moduleFixture.createNestApplication(); - await app.init(); - }); + app = moduleFixture.createNestApplication(); + await app.init(); + }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); }); From 2654a6929310abe36785a67398d342a387a283d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Thu, 4 Dec 2025 17:10:09 +0100 Subject: [PATCH 06/33] chore: fix all typescript import errors by massively creating NestJs services --- apps/server-nestjs/src/app.controller.spec.ts | 23 - .../src/cpin-module/old-server/src/app.ts | 22 +- .../src/cpin-module/old-server/src/connect.ts | 28 +- .../old-server/src/init/db/index.ts | 82 ++-- .../cpin-module/old-server/src/prepare-app.ts | 199 +++++---- .../src/resources/admin-role/router.ts | 138 +++--- .../src/resources/admin-token/router.ts | 88 ++-- .../src/resources/cluster/router.ts | 259 ++++++----- .../src/resources/environment/router.ts | 242 +++++----- .../old-server/src/resources/index.ts | 257 +++++++---- .../old-server/src/resources/log/router.ts | 53 ++- .../src/resources/project-member/router.ts | 212 ++++----- .../src/resources/project-role/router.ts | 224 +++++----- .../src/resources/project-service/router.ts | 114 ++--- .../src/resources/project/router.ts | 416 +++++++++--------- .../src/resources/repository/router.ts | 340 +++++++------- .../src/resources/service-chain/router.ts | 157 +++---- .../src/resources/service-monitor/router.ts | 84 ++-- .../old-server/src/resources/stage/router.ts | 162 +++---- .../src/resources/system/config/router.ts | 60 +-- .../old-server/src/resources/system/router.ts | 38 +- .../src/resources/system/settings/router.ts | 48 +- .../old-server/src/resources/user/router.ts | 94 ++-- .../src/resources/user/tokens/router.ts | 98 +++-- .../old-server/src/resources/zone/router.ts | 122 ++--- .../src/cpin-module/old-server/src/server.ts | 26 +- .../old-server/src/utils/fastify.ts | 82 ++-- .../old-server/src/utils/logger.ts | 181 ++++---- .../src/cpin-module/old-server/tsconfig.json | 26 -- 29 files changed, 2062 insertions(+), 1813 deletions(-) delete mode 100644 apps/server-nestjs/src/app.controller.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/tsconfig.json diff --git a/apps/server-nestjs/src/app.controller.spec.ts b/apps/server-nestjs/src/app.controller.spec.ts deleted file mode 100644 index 4964aaf9d..000000000 --- a/apps/server-nestjs/src/app.controller.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts index 2513e5717..b64a34d2b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts @@ -11,15 +11,21 @@ import type { FastifyRequest } from 'fastify'; import fastify from 'fastify'; import keycloak from 'fastify-keycloak-adapter'; -import { apiRouter } from './resources/index.js'; +import { ResourcesService } from './resources/index.js'; import { isDev, isInt, isTest } from './utils/env.js'; -import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify.js'; +import { FastifyService } from './utils/fastify.js'; import { keycloakConf, sessionConf } from './utils/keycloak.js'; import type { CustomLogger } from './utils/logger.js'; -import { log } from './utils/logger.js'; +import { LoggerService } from './utils/logger.js'; @Injectable() export class AppService { + constructor( + private readonly loggerService: LoggerService, + private readonly fastifyService: FastifyService, + private readonly resourcesService: ResourcesService, + ) {} + serverInstance: ReturnType = initServer(); app: any; @@ -27,7 +33,7 @@ export class AppService { async init() { const contract = await getContract(); - this.app = fastify(fastifyConf) + this.app = fastify(this.fastifyService.fastifyConf) .register(helmet, () => ({ contentSecurityPolicy: !(isInt || isDev || isTest), })) @@ -37,12 +43,12 @@ export class AppService { .register(keycloak, keycloakConf) .register(fastifySwagger, { transformObject: () => - generateOpenApi(contract, swaggerConf, { + generateOpenApi(contract, this.fastifyService.swaggerConf, { setOperationId: true, }), }) - .register(fastifySwaggerUi, swaggerUiConf) - .register(apiRouter()) + .register(fastifySwaggerUi, this.fastifyService.swaggerUiConf) + .register(this.resourcesService.apiRouter()) .addHook('onRoute', (opts) => { if (opts.path === `${apiPrefix}/healthz`) { opts.logLevel = 'silent'; @@ -57,7 +63,7 @@ export class AppService { error: message, stack: error.stack, }); - log('info', { reqId: req.id, error }); + this.loggerService.log('info', { reqId: req.id, error }); }) .addHook('onResponse', (req, res) => { if (res.statusCode < 400) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts index 309f8fa97..f7d8ea5d4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts @@ -1,12 +1,14 @@ import { Injectable } from '@nestjs/common'; import { setTimeout } from 'node:timers/promises'; -import { logger } from './app.js'; +import { AppService } from './app.js'; import prisma from './prisma.js'; import { dbUrl, isCI, isDev, isTest } from './utils/env.js'; @Injectable() export class ConnectionService { + constructor(private readonly appService: AppService) {} + closingConnections = false; async getConnection(triesLeft = 5): Promise { @@ -17,22 +19,30 @@ export class ConnectionService { try { if (isDev || isTest || isCI) { - logger.info(`Trying to connect to Postgres with: ${dbUrl}`); + this.appService.logger.info( + `Trying to connect to Postgres with: ${dbUrl}`, + ); } await prisma.$connect(); - logger.info('Connected to Postgres!'); + this.appService.logger.info('Connected to Postgres!'); } catch (error) { if (triesLeft > 0) { - logger.error(error); - logger.info(`Could not connect to Postgres: ${error.message}`); - logger.info(`Retrying (${triesLeft} tries left)`); + this.appService.logger.error(error); + this.appService.logger.info( + `Could not connect to Postgres: ${error.message}`, + ); + this.appService.logger.info( + `Retrying (${triesLeft} tries left)`, + ); await setTimeout(isTest || isCI ? 1000 : 10000); return this.getConnection(triesLeft); } - logger.info(`Could not connect to Postgres: ${error.message}`); - logger.info('Out of retries'); + this.appService.logger.info( + `Could not connect to Postgres: ${error.message}`, + ); + this.appService.logger.info('Out of retries'); error.message = `Out of retries, last error: ${error.message}`; throw error; } @@ -43,7 +53,7 @@ export class ConnectionService { try { await prisma.$disconnect(); } catch (error) { - logger.error(error); + this.appService.logger.error(error); } finally { this.closingConnections = false; } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts index c57d6706f..c2e0e4112 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts @@ -1,4 +1,5 @@ -import { logger } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import prisma from '@old-server/prisma.js'; import { modelKeys } from './utils.js'; @@ -13,46 +14,53 @@ type Imports = Partial> & { associations: [Models, any[]]; }; -export async function initDb(data: Imports) { - const dataStringified = JSON.stringify(data); - const dataParsed = JSON.parse(dataStringified, (key, value) => { - try { - if (['permissions', 'everyonePerms'].includes(key)) { - return BigInt(value.slice(0, value.length - 1)); +@Injectable() +export class InitDBService { + constructor(private readonly appService: AppService) {} + + async initDb(data: Imports) { + const dataStringified = JSON.stringify(data); + const dataParsed = JSON.parse(dataStringified, (key, value) => { + try { + if (['permissions', 'everyonePerms'].includes(key)) { + return BigInt(value.slice(0, value.length - 1)); + } + } catch (_error) { + return value; } - } catch (_error) { return value; + }); + this.appService.logger.info('Drop tables'); + for (const modelKey of modelKeys.toReversed()) { + // @ts-ignore + await prisma[modelKey].deleteMany(); } - return value; - }); - logger.info('Drop tables'); - for (const modelKey of modelKeys.toReversed()) { - // @ts-ignore - await prisma[modelKey].deleteMany(); - } - logger.info('Import models'); - for (const modelKey of modelKeys) { - // @ts-ignore - await prisma[modelKey].createMany({ data: dataParsed[modelKey] }); - } - logger.info('Import associations'); - for (const [modelKey, rows] of dataParsed.associations) { - for (const row of rows) { - const idKey = 'id'; - const connectKeys = Object.keys(row).filter((key) => key !== idKey); - const dataConnects = connectKeys.reduce( - (acc, curr) => { - acc[curr] = { connect: row[curr] }; - return acc; - }, - {} as Record, - ); + this.appService.logger.info('Import models'); + for (const modelKey of modelKeys) { // @ts-ignore - await prisma[modelKey].update({ - where: { id: row.id }, - data: dataConnects, - }); + await prisma[modelKey].createMany({ data: dataParsed[modelKey] }); + } + this.appService.logger.info('Import associations'); + for (const [modelKey, rows] of dataParsed.associations) { + for (const row of rows) { + const idKey = 'id'; + const connectKeys = Object.keys(row).filter( + (key) => key !== idKey, + ); + const dataConnects = connectKeys.reduce( + (acc, curr) => { + acc[curr] = { connect: row[curr] }; + return acc; + }, + {} as Record, + ); + // @ts-ignore + await prisma[modelKey].update({ + where: { id: row.id }, + data: dataConnects, + }); + } } + this.appService.logger.info('End import'); } - logger.info('End import'); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts index f17f5648d..465861e26 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts @@ -1,107 +1,126 @@ -import { rm } from 'node:fs/promises'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { Injectable } from '@nestjs/common'; import { ProxyAgent, setGlobalDispatcher } from 'undici'; -import app, { logger } from './app'; -import { getConnection } from './connect'; -import { initDb } from './init/db/index'; +import { AppService } from './app'; +import { ConnectionService } from './connect'; +import { InitDBService } from './init/db'; import { initPm } from './plugins'; -import { isCI, isDev, isDevSetup, isInt, isProd, isTest } from './utils/env'; +import { isCI, isDev, isDevSetup, isProd, isTest } from './utils/env'; -// Workaround because fetch isn't using http_proxy variables -// See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 -if (process.env.HTTP_PROXY) { - setGlobalDispatcher(new ProxyAgent(process.env.HTTP_PROXY)); -} - -async function initializeDB(path: string) { - logger.info('Starting init DB...'); - const { data } = await import(path); - await initDb(data); - logger.info('initDb invoked successfully'); -} +@Injectable() +export class PrepareAppService { + constructor( + private readonly appService: AppService, + private readonly connectionService: ConnectionService, + private readonly initDBService: InitDBService, + ) { + // Workaround because fetch isn't using http_proxy variables + // See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 + if (process.env.HTTP_PROXY) { + setGlobalDispatcher(new ProxyAgent(process.env.HTTP_PROXY)); + } + } -export async function startServer() { - try { - await getConnection(); - } catch (error) { - if (!(error instanceof Error)) return; - logger.error(error.message); - throw error; + async initializeDB(path: string) { + this.appService.logger.info('Starting init DB...'); + const { data } = await import(path); + await this.initDBService.initDb(data); + this.appService.logger.info('initDb invoked successfully'); } - initPm(); + async startServer() { + try { + await this.connectionService.getConnection(); + } catch (error) { + if (!(error instanceof Error)) return; + this.appService.logger.error(error.message); + throw error; + } - logger.info('Reading init database file'); + initPm(); - // try { - // const dataPath = - // isProd || isInt - // ? './init/db/imports/data.js' - // : '@cpn-console/test-utils/src/imports/data.ts'; - // await initializeDB(dataPath); - // if (isProd && !isDevSetup) { - // logger.info('Cleaning up imported data file...'); - // const __filename = fileURLToPath(import.meta.url); - // const __dirname = dirname(__filename); - // await rm(resolve(__dirname, dataPath)); - // logger.info(`Successfully deleted '${dataPath}'`); - // } - // } catch (error) { - // if ( - // error.code === 'ERR_MODULE_NOT_FOUND' || - // error.message.includes('Failed to load') || - // error.message.includes('Cannot find module') - // ) { - // logger.info('No initDb file, skipping'); - // } else { - // logger.warn(error.message); - // throw error; - // } - // } + this.appService.logger.info('Reading init database file'); - logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }); -} + // try { + // const dataPath = + // isProd || isInt + // ? './init/db/imports/data.js' + // : '@cpn-console/test-utils/src/imports/data.ts'; + // await initializeDB(dataPath); + // if (isProd && !isDevSetup) { + // this.appService.logger.info('Cleaning up imported data file...'); + // const __filename = fileURLToPath(import.meta.url); + // const __dirname = dirname(__filename); + // await rm(resolve(__dirname, dataPath)); + // this.appService.logger.info(`Successfully deleted '${dataPath}'`); + // } + // } catch (error) { + // if ( + // error.code === 'ERR_MODULE_NOT_FOUND' || + // error.message.includes('Failed to load') || + // error.message.includes('Cannot find module') + // ) { + // this.appService.logger.info('No initDb file, skipping'); + // } else { + // this.appService.logger.warn(error.message); + // throw error; + // } + // } -export async function getPreparedApp() { - try { - await getConnection(); - } catch (error) { - logger.error(error.message); - throw error; + this.appService.logger.debug({ + isDev, + isTest, + isCI, + isDevSetup, + isProd, + }); } - initPm(); + async getPreparedApp() { + try { + await this.connectionService.getConnection(); + } catch (error) { + this.appService.logger.error(error.message); + throw error; + } + + initPm(); - logger.info('Reading init database file'); + this.appService.logger.info('Reading init database file'); - // try { - // const dataPath = - // isProd || isInt - // ? './init/db/imports/data.js' - // : '@cpn-console/test-utils/src/imports/data.ts'; - // await initializeDB(dataPath); - // if (isProd && !isDevSetup) { - // logger.info('Cleaning up imported data file...'); - // const __filename = fileURLToPath(import.meta.url); - // const __dirname = dirname(__filename); - // await rm(resolve(__dirname, dataPath)); - // logger.info(`Successfully deleted '${dataPath}'`); - // } - // } catch (error) { - // if ( - // error.code === 'ERR_MODULE_NOT_FOUND' || - // error.message.includes('Failed to load') || - // error.message.includes('Cannot find module') - // ) { - // logger.info('No initDb file, skipping'); - // } else { - // logger.warn(error.message); - // throw error; - // } - // } + // try { + // const dataPath = + // isProd || isInt + // ? './init/db/imports/data.js' + // : '@cpn-console/test-utils/src/imports/data.ts'; + // await initializeDB(dataPath); + // if (isProd && !isDevSetup) { + // this.appService.logger.info('Cleaning up imported data file...'); + // const __filename = fileURLToPath(import.meta.url); + // const __dirname = dirname(__filename); + // await rm(resolve(__dirname, dataPath)); + // this.appService.logger.info(`Successfully deleted '${dataPath}'`); + // } + // } catch (error) { + // if ( + // error.code === 'ERR_MODULE_NOT_FOUND' || + // error.message.includes('Failed to load') || + // error.message.includes('Cannot find module') + // ) { + // this.appService.logger.info('No initDb file, skipping'); + // } else { + // this.appService.logger.warn(error.message); + // throw error; + // } + // } - logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }); - return app; + this.appService.logger.debug({ + isDev, + isTest, + isCI, + isDevSetup, + isProd, + }); + return this.appService.app; + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts index 9d546453a..ee2910b7f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts @@ -1,5 +1,6 @@ import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; @@ -11,69 +12,74 @@ import { patchRoles, } from './business.js'; -export function adminRoleRouter() { - return serverInstance.router(adminRoleContract, { - // Récupérer des projets - listAdminRoles: async () => { - const body = await listRoles(); - - return { - status: 200, - body, - }; - }, - - createAdminRole: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await createRole(body); - - return { - status: 201, - body: resBody, - }; - }, - - patchAdminRoles: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await patchRoles(body); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - adminRoleMemberCounts: async ({ request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await countRolesMembers(); - - return { - status: 200, - body: resBody, - }; - }, - - deleteAdminRole: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await deleteRole(params.roleId); - - return { - status: 204, - body: resBody, - }; - }, - }); +@Injectable() +export class AdminRoleRouterService { + constructor(private readonly appService: AppService) {} + + adminRoleRouter() { + return this.appService.serverInstance.router(adminRoleContract, { + // Récupérer des projets + listAdminRoles: async () => { + const body = await listRoles(); + + return { + status: 200, + body, + }; + }, + + createAdminRole: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await createRole(body); + + return { + status: 201, + body: resBody, + }; + }, + + patchAdminRoles: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await patchRoles(body); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 200, + body: resBody, + }; + }, + + adminRoleMemberCounts: async ({ request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await countRolesMembers(); + + return { + status: 200, + body: resBody, + }; + }, + + deleteAdminRole: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await deleteRole(params.roleId); + + return { + status: 204, + body: resBody, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts index 1869769bd..670ce3ffa 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts @@ -1,48 +1,54 @@ import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; -import { serverInstance } from '../../app.js'; +import { AppService } from '../../app.js'; import { createToken, deleteToken, listTokens } from './business.js'; -export function adminTokenRouter() { - return serverInstance.router(adminTokenContract, { - listAdminTokens: async ({ request: req, query }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await listTokens(query); - - return { - status: 200, - body, - }; - }, - - createAdminToken: async ({ request: req, body: data }) => { - const perms = await authUser(req); - - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await createToken(data); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - deleteAdminToken: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - await deleteToken(params.tokenId); - - return { - status: 204, - body: null, - }; - }, - }); +@Injectable() +export class AdminTokenRouterService { + constructor(private readonly appService: AppService) {} + + adminTokenRouter() { + return this.appService.serverInstance.router(adminTokenContract, { + listAdminTokens: async ({ request: req, query }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await listTokens(query); + + return { + status: 200, + body, + }; + }, + + createAdminToken: async ({ request: req, body: data }) => { + const perms = await authUser(req); + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await createToken(data); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + deleteAdminToken: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + await deleteToken(params.tokenId); + + return { + status: 204, + body: null, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts index 740cde7fc..55057f70e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts @@ -1,6 +1,7 @@ import type { AsyncReturnType } from '@cpn-console/shared'; import { AdminAuthorized, clusterContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import '@old-server/types/index.js'; import { authUser } from '@old-server/utils/controller.js'; import { @@ -19,126 +20,142 @@ import { updateCluster, } from './business.js'; -export function clusterRouter() { - return serverInstance.router(clusterContract, { - listClusters: async ({ request: req }) => { - const { adminPermissions, user } = await authUser(req); - - let body: AsyncReturnType = []; - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listClusters(); - } else if (user) { - body = await listClusters(user.id); - } - - return { - status: 200, - body, - }; - }, - - getClusterDetails: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const cluster = await getClusterDetailsBusiness(clusterId); - - return { - status: 200, - body: cluster, - }; - }, - - getClusterUsage: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const usage = await getClusterUsage(clusterId); - - return { - status: 200, - body: usage, - }; - }, - - createCluster: async ({ request: req, body: data }) => { - const { adminPermissions, user } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - const body = await createCluster(data, user.id, req.id); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - getClusterEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const environments = - await getClusterAssociatedEnvironments(clusterId); - - return { - status: 200, - body: environments, - }; - }, - - updateCluster: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', +@Injectable() +export class ClusterRouterService { + constructor(private readonly appService: AppService) {} + + clusterRouter() { + return this.appService.serverInstance.router(clusterContract, { + listClusters: async ({ request: req }) => { + const { adminPermissions, user } = await authUser(req); + + let body: AsyncReturnType = []; + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listClusters(); + } else if (user) { + body = await listClusters(user.id); + } + + return { + status: 200, + body, + }; + }, + + getClusterDetails: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const cluster = await getClusterDetailsBusiness(clusterId); + + return { + status: 200, + body: cluster, + }; + }, + + getClusterUsage: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const usage = await getClusterUsage(clusterId); + + return { + status: 200, + body: usage, + }; + }, + + createCluster: async ({ request: req, body: data }) => { + const { adminPermissions, user } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + const body = await createCluster(data, user.id, req.id); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + getClusterEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const environments = + await getClusterAssociatedEnvironments(clusterId); + + return { + status: 200, + body: environments, + }; + }, + + updateCluster: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + + const clusterId = params.clusterId; + const body = await updateCluster( + data, + clusterId, + user.id, + req.id, ); - const clusterId = params.clusterId; - const body = await updateCluster(data, clusterId, user.id, req.id); - - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - deleteCluster: async ({ request: req, params, query: { force } }) => { - const { user, adminPermissions, tokenId } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user?.id && !tokenId) - return new Unauthorized401('Your identity has not been found'); - - const clusterId = params.clusterId; - const body = await deleteCluster({ - clusterId, - userId: user?.id, - requestId: req.id, - force, - }); - - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + deleteCluster: async ({ + request: req, + params, + query: { force }, + }) => { + const { user, adminPermissions, tokenId } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user?.id && !tokenId) + return new Unauthorized401( + 'Your identity has not been found', + ); + + const clusterId = params.clusterId; + const body = await deleteCluster({ + clusterId, + userId: user?.id, + requestId: req.id, + force, + }); + + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts index b8ab3ee4a..2cf9a9b11 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts @@ -1,5 +1,6 @@ import { ProjectAuthorized, environmentContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { BadRequest400, @@ -18,133 +19,138 @@ import { updateEnvironment, } from './business.js'; -export function environmentRouter() { - return serverInstance.router(environmentContract, { - listEnvironments: async ({ request: req, query }) => { - const projectId = query.projectId; - const perms = await authUser(req, { id: projectId }); +@Injectable() +export class EnvironmentRouterService { + constructor(private readonly appService: AppService) {} - const environments = ProjectAuthorized.ListEnvironments(perms) - ? await getProjectEnvironments(projectId) - : []; + environmentRouter() { + return this.appService.serverInstance.router(environmentContract, { + listEnvironments: async ({ request: req, query }) => { + const projectId = query.projectId; + const perms = await authUser(req, { id: projectId }); - return { - status: 200, - body: environments, - }; - }, + const environments = ProjectAuthorized.ListEnvironments(perms) + ? await getProjectEnvironments(projectId) + : []; - createEnvironment: async ({ request: req, body: requestBody }) => { - const projectId = requestBody.projectId; - const perms = await authUser(req, { id: projectId }); + return { + status: 200, + body: environments, + }; + }, - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); + createEnvironment: async ({ request: req, body: requestBody }) => { + const projectId = requestBody.projectId; + const perms = await authUser(req, { id: projectId }); - const checkCreateResult = await checkEnvironmentCreate({ - ...requestBody, - }); - if (checkCreateResult.isError) - return new BadRequest400(checkCreateResult.error); + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); - const result = await createEnvironment({ - userId: perms.user.id, - projectId, - name: requestBody.name, - clusterId: requestBody.clusterId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - stageId: requestBody.stageId, - requestId: req.id, - }); - if (result.isError) { - return new Internal500(result.error); - } - return { - status: 201, - body: result.data, - }; - }, + const checkCreateResult = await checkEnvironmentCreate({ + ...requestBody, + }); + if (checkCreateResult.isError) + return new BadRequest400(checkCreateResult.error); - updateEnvironment: async ({ - request: req, - body: requestBody, - params, - }) => { - const { environmentId } = params; - const perms = await authUser(req, { environmentId }); - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if (!ProjectAuthorized.ListEnvironments(perms)) - return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); + const result = await createEnvironment({ + userId: perms.user.id, + projectId, + name: requestBody.name, + clusterId: requestBody.clusterId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + stageId: requestBody.stageId, + requestId: req.id, + }); + if (result.isError) { + return new Internal500(result.error); + } + return { + status: 201, + body: result.data, + }; + }, - const checkUpdateResult = await checkEnvironmentUpdate({ - environmentId, - ...requestBody, - }); - if (checkUpdateResult.isError) - return new BadRequest400(checkUpdateResult.error); + updateEnvironment: async ({ + request: req, + body: requestBody, + params, + }) => { + const { environmentId } = params; + const perms = await authUser(req, { environmentId }); + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!ProjectAuthorized.ListEnvironments(perms)) + return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); - const result = await updateEnvironment({ - user: perms.user, - environmentId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - requestId: req.id, - }); - if (result.isError) { - return new Internal500(result.error); - } - return { - status: 200, - body: result.data, - }; - }, + const checkUpdateResult = await checkEnvironmentUpdate({ + environmentId, + ...requestBody, + }); + if (checkUpdateResult.isError) + return new BadRequest400(checkUpdateResult.error); - deleteEnvironment: async ({ request: req, params }) => { - const { environmentId } = params; - const perms = await authUser(req, { environmentId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); + const result = await updateEnvironment({ + user: perms.user, + environmentId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + requestId: req.id, + }); + if (result.isError) { + return new Internal500(result.error); + } + return { + status: 200, + body: result.data, + }; + }, - const result = await deleteEnvironment({ - userId: perms.user?.id, - environmentId, - requestId: req.id, - projectId: perms.projectId, - }); - if (result.isError) { - return new Internal500(result.error); - } + deleteEnvironment: async ({ request: req, params }) => { + const { environmentId } = params; + const perms = await authUser(req, { environmentId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); - return { - status: 204, - body: result.data, - }; - }, - }); + const result = await deleteEnvironment({ + userId: perms.user?.id, + environmentId, + requestId: req.id, + projectId: perms.projectId, + }); + if (result.isError) { + return new Internal500(result.error); + } + + return { + status: 204, + body: result.data, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts index 15735c4e8..99af62fec 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts @@ -1,93 +1,174 @@ -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app'; import type { FastifyInstance } from 'fastify'; -import { adminRoleRouter } from './admin-role/router.js'; -import { adminTokenRouter } from './admin-token/router.js'; -import { clusterRouter } from './cluster/router.js'; -import { environmentRouter } from './environment/router.js'; -import { logRouter } from './log/router.js'; -import { projectMemberRouter } from './project-member/router.js'; -import { projectRoleRouter } from './project-role/router.js'; -import { projectServiceRouter } from './project-service/router.js'; -import { projectRouter } from './project/router.js'; -import { repositoryRouter } from './repository/router.js'; -import { serviceChainRouter } from './service-chain/router.js'; -import { serviceMonitorRouter } from './service-monitor/router.js'; -import { stageRouter } from './stage/router.js'; -import { pluginConfigRouter } from './system/config/router.js'; -import { systemRouter } from './system/router.js'; -import { systemSettingsRouter } from './system/settings/router.js'; -import { userRouter } from './user/router.js'; -import { personalAccessTokenRouter } from './user/tokens/router.js'; -import { zoneRouter } from './zone/router.js'; +import { AdminRoleRouterService } from './admin-role/router'; +import { AdminTokenRouterService } from './admin-token/router'; +import { ClusterRouterService } from './cluster/router'; +import { EnvironmentRouterService } from './environment/router'; +import { LogRouterService } from './log/router'; +import { ProjectMemberRouterService } from './project-member/router'; +import { ProjectRoleRouterService } from './project-role/router'; +import { ProjectServiceRouterService } from './project-service/router'; +import { ProjectRouterService } from './project/router'; +import { RepositoryRouterService } from './repository/router'; +import { ServiceChainRouterService } from './service-chain/router'; +import { ServiceMonitorRouterService } from './service-monitor/router'; +import { StageRouterService } from './stage/router'; +import { SystemConfigRouterService } from './system/config/router'; +import { SystemRouterService } from './system/router'; +import { SystemSettingsRouterService } from './system/settings/router'; +import { UserRouterService } from './user/router'; +import { UserTokensRouterService } from './user/tokens/router'; +import { ZoneRouterService } from './zone/router'; -// relax validation schema if NO_VALIDATION env var is set to true. -// /!\ It can lead to security leaks !!!! -const validateTrue = { - responseValidation: process.env.NO_VALIDATION !== 'true', -}; -export function apiRouter() { - return async (app: FastifyInstance) => { - await app.register( - serverInstance.plugin(adminRoleRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(adminTokenRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(clusterRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(serviceChainRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(environmentRouter()), - validateTrue, - ); - await app.register(serverInstance.plugin(logRouter()), validateTrue); - await app.register( - serverInstance.plugin(personalAccessTokenRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(projectRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(projectMemberRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(projectRoleRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(projectServiceRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(repositoryRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(serviceMonitorRouter()), - validateTrue, - ); - await app.register( - serverInstance.plugin(pluginConfigRouter()), - validateTrue, - ); - await app.register(serverInstance.plugin(stageRouter()), validateTrue); - await app.register(serverInstance.plugin(systemRouter()), validateTrue); - await app.register( - serverInstance.plugin(systemSettingsRouter()), - validateTrue, - ); - await app.register(serverInstance.plugin(userRouter()), validateTrue); - await app.register(serverInstance.plugin(zoneRouter()), validateTrue); +@Injectable() +export class ResourcesService { + constructor( + private readonly appService: AppService, + private readonly adminRoleRouterService: AdminRoleRouterService, + private readonly adminTokenRouterService: AdminTokenRouterService, + private readonly clusterRouterService: ClusterRouterService, + private readonly environmentRouterService: EnvironmentRouterService, + private readonly logRouterService: LogRouterService, + private readonly projectMemberRouterService: ProjectMemberRouterService, + private readonly projectRoleRouterService: ProjectRoleRouterService, + private readonly projectServiceRouterService: ProjectServiceRouterService, + private readonly projectRouterService: ProjectRouterService, + private readonly repositoryRouterService: RepositoryRouterService, + private readonly serviceChainRouterService: ServiceChainRouterService, + private readonly serviceMonitorRouterService: ServiceMonitorRouterService, + private readonly stageRouterService: StageRouterService, + private readonly systemConfigRouterService: SystemConfigRouterService, + private readonly systemRouterService: SystemRouterService, + private readonly systemSettingsRouterService: SystemSettingsRouterService, + private readonly userRouterService: UserRouterService, + private readonly userTokensRouterService: UserTokensRouterService, + private readonly zoneRouterService: ZoneRouterService, + ) {} + + // relax validation schema if NO_VALIDATION env var is set to true. + // /!\ It can lead to security leaks !!!! + validateTrue = { + responseValidation: process.env.NO_VALIDATION !== 'true', }; + + apiRouter() { + return async (app: FastifyInstance) => { + await app.register( + this.appService.serverInstance.plugin( + this.adminRoleRouterService.adminRoleRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.adminTokenRouterService.adminTokenRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.clusterRouterService.clusterRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.serviceChainRouterService.serviceChainRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.environmentRouterService.environmentRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.logRouterService.logRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.userTokensRouterService.personalAccessTokenRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.projectRouterService.projectRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.projectMemberRouterService.projectMemberRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.projectRoleRouterService.projectRoleRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.projectServiceRouterService.projectServiceRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.repositoryRouterService.repositoryRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.serviceMonitorRouterService.serviceMonitorRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.systemConfigRouterService.pluginConfigRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.stageRouterService.stageRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.systemRouterService.systemRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.systemSettingsRouterService.systemSettingsRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.userRouterService.userRouter(), + ), + this.validateTrue, + ); + await app.register( + this.appService.serverInstance.plugin( + this.zoneRouterService.zoneRouter(), + ), + this.validateTrue, + ); + }; + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts index 4823b3de2..7de8f8bf0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts @@ -1,6 +1,7 @@ import type { CleanLog, Log, XOR } from '@cpn-console/shared'; import { AdminAuthorized, logContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import type { UserProfile, UserProjectProfile, @@ -10,30 +11,36 @@ import { Forbidden403 } from '@old-server/utils/errors.js'; import { getLogs } from './business.js'; -export function logRouter() { - return serverInstance.router(logContract, { - // Récupérer des logs - getLogs: async ({ request: req, query }) => { - const perms: XOR = query.projectId - ? await authUser(req, { id: query.projectId }) - : await authUser(req); +@Injectable() +export class LogRouterService { + constructor(private readonly appService: AppService) {} - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { - if (!perms.projectPermissions) { - return new Forbidden403(); + logRouter() { + return this.appService.serverInstance.router(logContract, { + // Récupérer des logs + getLogs: async ({ request: req, query }) => { + const perms: XOR = + query.projectId + ? await authUser(req, { id: query.projectId }) + : await authUser(req); + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { + if (!perms.projectPermissions) { + return new Forbidden403(); + } + query.clean = true; } - query.clean = true; - } - const [total, logs] = (await getLogs(query)) as [ - number, - unknown[], - ] as [number, Array]; + const [total, logs] = (await getLogs(query)) as [ + number, + unknown[], + ] as [number, Array]; - return { - status: 200, - body: { total, logs }, - }; - }, - }); + return { + status: 200, + body: { total, logs }, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts index 213be32d6..0e9038fa6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts @@ -3,7 +3,8 @@ import { ProjectAuthorized, projectMemberContract, } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, @@ -19,107 +20,112 @@ import { removeMember, } from './business.js'; -export function projectMemberRouter() { - return serverInstance.router(projectMemberContract, { - listMembers: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - - const body = await listMembers(projectId); - - return { - status: 200, - body, - }; - }, - - addMember: async ({ request: req, params, body }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', +@Injectable() +export class ProjectMemberRouterService { + constructor(private readonly appService: AppService) {} + + projectMemberRouter() { + return this.appService.serverInstance.router(projectMemberContract, { + listMembers: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const body = await listMembers(projectId); + + return { + status: 200, + body, + }; + }, + + addMember: async ({ request: req, params, body }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageMembers(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await addMember( + projectId, + body, + perms.user.id, + req.id, + perms.projectOwnerId, ); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if (!ProjectAuthorized.ManageMembers(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await addMember( - projectId, - body, - perms.user.id, - req.id, - perms.projectOwnerId, - ); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 201, - body: resBody, - }; - }, - - patchMembers: async ({ request: req, params, body }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageMembers(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await patchMembers(projectId, body); - - return { - status: 200, - body: resBody, - }; - }, - - removeMember: async ({ request: req, params }) => { - const { projectId, userId } = params; - const perms = await authUser(req, { id: projectId }); - - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - - if ( - !ProjectAuthorized.ManageMembers(perms) && - userId !== perms.user?.id - ) - return new Forbidden403(); - - const resBody = await removeMember(projectId, params.userId); - - return { - status: 200, - body: resBody, - }; - }, - }); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 201, + body: resBody, + }; + }, + + patchMembers: async ({ request: req, params, body }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageMembers(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await patchMembers(projectId, body); + + return { + status: 200, + body: resBody, + }; + }, + + removeMember: async ({ request: req, params }) => { + const { projectId, userId } = params; + const perms = await authUser(req, { id: projectId }); + + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + if ( + !ProjectAuthorized.ManageMembers(perms) && + userId !== perms.user?.id + ) + return new Forbidden403(); + + const resBody = await removeMember(projectId, params.userId); + + return { + status: 200, + body: resBody, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts index 729ec6956..aa872f2e6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts @@ -3,7 +3,8 @@ import { ProjectAuthorized, projectRoleContract, } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, @@ -19,113 +20,118 @@ import { patchRoles, } from './business.js'; -export function projectRoleRouter() { - return serverInstance.router(projectRoleContract, { - // Récupérer des projets - listProjectRoles: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - - const body = await listRoles(projectId); - - return { - status: 200, +@Injectable() +export class ProjectRoleRouterService { + constructor(private readonly appService: AppService) {} + + projectRoleRouter() { + return this.appService.serverInstance.router(projectRoleContract, { + // Récupérer des projets + listProjectRoles: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const body = await listRoles(projectId); + + return { + status: 200, + body, + }; + }, + + createProjectRole: async ({ + request: req, + params: { projectId }, body, - }; - }, - - createProjectRole: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await createRole(projectId, body); - - return { - status: 201, - body: resBody, - }; - }, - - patchProjectRoles: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await patchRoles(projectId, body); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - projectRoleMemberCounts: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - - const resBody = await countRolesMembers(projectId); - - return { - status: 200, - body: resBody, - }; - }, - - deleteProjectRole: async ({ - request: req, - params: { projectId, roleId }, - }) => { - const perms = await authUser(req, { id: projectId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await deleteRole(roleId); - - return { - status: 204, - body: resBody, - }; - }, - }); + }) => { + const perms = await authUser(req, { id: projectId }); + + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await createRole(projectId, body); + + return { + status: 201, + body: resBody, + }; + }, + + patchProjectRoles: async ({ + request: req, + params: { projectId }, + body, + }) => { + const perms = await authUser(req, { id: projectId }); + + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await patchRoles(projectId, body); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 200, + body: resBody, + }; + }, + + projectRoleMemberCounts: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const resBody = await countRolesMembers(projectId); + + return { + status: 200, + body: resBody, + }; + }, + + deleteProjectRole: async ({ + request: req, + params: { projectId, roleId }, + }) => { + const perms = await authUser(req, { id: projectId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await deleteRole(roleId); + + return { + status: 204, + body: resBody, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts index 5060c4914..445962628 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts @@ -3,67 +3,77 @@ import { ProjectAuthorized, projectServiceContract, } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { Forbidden403, NotFound404 } from '@old-server/utils/errors.js'; import { getProjectServices, updateProjectServices } from './business.js'; -export function projectServiceRouter() { - return serverInstance.router(projectServiceContract, { - // Récupérer les services d'un projet - getServices: async ({ request: req, params: { projectId }, query }) => { - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if ( - !AdminAuthorized.isAdmin(perms.adminPermissions) && - query.permissionTarget === 'admin' - ) - return new Forbidden403( - 'Vous ne pouvez pas demander les paramètres admin', +@Injectable() +export class ProjectServiceRouterService { + constructor(private readonly appService: AppService) {} + + projectServiceRouter() { + return this.appService.serverInstance.router(projectServiceContract, { + // Récupérer les services d'un projet + getServices: async ({ + request: req, + params: { projectId }, + query, + }) => { + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if ( + !AdminAuthorized.isAdmin(perms.adminPermissions) && + query.permissionTarget === 'admin' + ) + return new Forbidden403( + 'Vous ne pouvez pas demander les paramètres admin', + ); + + const body = await getProjectServices( + projectId, + query.permissionTarget, ); - const body = await getProjectServices( - projectId, - query.permissionTarget, - ); + return { + status: 200, + body, + }; + }, - return { - status: 200, + updateProjectServices: async ({ + request: req, + params: { projectId }, body, - }; - }, - - updateProjectServices: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - if (!ProjectAuthorized.Manage(perms)) return new NotFound404(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); + }) => { + const perms = await authUser(req, { id: projectId }); + if (!ProjectAuthorized.Manage(perms)) return new NotFound404(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); - const allowedRoles: Array<'user' | 'admin'> = - AdminAuthorized.isAdmin(perms.adminPermissions) - ? ['user', 'admin'] - : ['user']; + const allowedRoles: Array<'user' | 'admin'> = + AdminAuthorized.isAdmin(perms.adminPermissions) + ? ['user', 'admin'] + : ['user']; - const resBody = await updateProjectServices( - projectId, - body, - allowedRoles, - ); - return { - status: 204, - body: resBody, - }; - }, - }); + const resBody = await updateProjectServices( + projectId, + body, + allowedRoles, + ); + return { + status: 204, + body: resBody, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts index dea4256e1..80565b788 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts @@ -4,7 +4,8 @@ import { ProjectAuthorized, projectContract, } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { BadRequest400, @@ -26,205 +27,226 @@ import { updateProject, } from './business.js'; -export function projectRouter() { - return serverInstance.router(projectContract, { - // Récupérer des projets - listProjects: async ({ request: req, query }) => { - const { adminPermissions, user } = await authUser(req); - let body: AsyncReturnType = []; - - if (adminPermissions && !user) { - // c'est donc un compte de service - query.filter = 'all'; - } - if ( - query.filter === 'all' && - !AdminAuthorized.isAdmin(adminPermissions) - ) { - return new BadRequest400( - "Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre 'all'", - ); - } - - body = await listProjects(query, user?.id); - - return { - status: 200, - body, - }; - }, - - // Récupérer les secrets d'un projet - getProjectSecrets: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.SeeSecrets(perms)) return new Forbidden403(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await getProjectSecrets(projectId); - - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Créer un projet - createProject: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (perms.user?.type !== 'human') - return new Unauthorized401('Cannot find requestor in database'); - const body = await createProject(data, perms.user, req.id); - - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Récuperer un seul projet - getProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.projectId) return new NotFound404(); - if (!isAdmin) { - if (!perms.projectPermissions) { - return new NotFound404(); +@Injectable() +export class ProjectRouterService { + constructor(private readonly appService: AppService) {} + + projectRouter() { + return this.appService.serverInstance.router(projectContract, { + // Récupérer des projets + listProjects: async ({ request: req, query }) => { + const { adminPermissions, user } = await authUser(req); + let body: AsyncReturnType = []; + + if (adminPermissions && !user) { + // c'est donc un compte de service + query.filter = 'all'; } - if (perms.projectStatus === 'archived') { + if ( + query.filter === 'all' && + !AdminAuthorized.isAdmin(adminPermissions) + ) { + return new BadRequest400( + "Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre 'all'", + ); + } + + body = await listProjects(query, user?.id); + + return { + status: 200, + body, + }; + }, + + // Récupérer les secrets d'un projet + getProjectSecrets: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.SeeSecrets(perms)) + return new Forbidden403(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await getProjectSecrets(projectId); + + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Créer un projet + createProject: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (perms.user?.type !== 'human') + return new Unauthorized401( + 'Cannot find requestor in database', + ); + const body = await createProject(data, perms.user, req.id); + + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Récuperer un seul projet + getProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.projectId) return new NotFound404(); + if (!isAdmin) { + if (!perms.projectPermissions) { + return new NotFound404(); + } + if (perms.projectStatus === 'archived') { + return new NotFound404(); + } + } + + const body = await getProject(projectId); + + return { + status: 200, + body, + }; + }, + + // Mettre à jour un projet + updateProject: async ({ request: req, params, body: data }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Cannot find requestor in database', + ); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + const isOwner = perms.projectOwnerId === perms.user.id; + + if (!perms.projectPermissions && !isAdmin) return new NotFound404(); + if (!isAdmin) { + // filtrage des clés par niveau de permissions + delete data.locked; + if (!isOwner) { + delete data.ownerId; // impossible de toucher à cette clé + } } - } - - const body = await getProject(projectId); - - return { - status: 200, - body, - }; - }, - - // Mettre à jour un projet - updateProject: async ({ request: req, params, body: data }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) - return new Unauthorized401('Cannot find requestor in database'); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - const isOwner = perms.projectOwnerId === perms.user.id; - - if (!perms.projectPermissions && !isAdmin) return new NotFound404(); - if (!isAdmin) { - // filtrage des clés par niveau de permissions - delete data.locked; - if (!isOwner) { - delete data.ownerId; // impossible de toucher à cette clé + if (perms.projectLocked) { + if (!isAdmin) + return new Forbidden403('Le projet est verrouillé'); + if (data.locked !== false) + return new Forbidden403( + 'Veuillez déverrouiler le projet pour le mettre à jour', + ); } - } - if (perms.projectLocked) { - if (!isAdmin) - return new Forbidden403('Le projet est verrouillé'); - if (data.locked !== false) - return new Forbidden403( - 'Veuillez déverrouiler le projet pour le mettre à jour', + + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); + + const body = await updateProject( + data, + projectId, + perms.user, + req.id, + ); + + if (body instanceof ErrorResType) return body; + return { + status: 200, + body, + }; + }, + + // Reprovisionner un projet + replayHooksForProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.projectPermissions && !isAdmin) + return new NotFound404(); + if (!ProjectAuthorized.ReplayHooks(perms)) + return new Forbidden403(); + + const body = await replayHooks({ + projectId, + userId: perms.user?.id, + requestId: req.id, + }); + + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + + // Archiver un projet + archiveProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.user) + return new Unauthorized401( + 'Cannot find requestor in database', + ); + if (!perms.projectPermissions && !isAdmin) + return new NotFound404(); + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); + + const body = await archiveProject( + projectId, + perms.user, + req.id, + ); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + // Récupérer les données de tous les projets pour export + getProjectsData: async ({ request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await generateProjectsData(); + + return { + status: 200, + body, + }; + }, + + bulkActionProject: async ({ request: req, body }) => { + const perms = await authUser(req); + + if (!perms.user) + return new Unauthorized401( + 'Cannot find requestor in database', ); - } - - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); - - const body = await updateProject( - data, - projectId, - perms.user, - req.id, - ); - - if (body instanceof ErrorResType) return body; - return { - status: 200, - body, - }; - }, - - // Reprovisionner un projet - replayHooksForProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.projectPermissions && !isAdmin) return new NotFound404(); - if (!ProjectAuthorized.ReplayHooks(perms)) - return new Forbidden403(); - - const body = await replayHooks({ - projectId, - userId: perms.user?.id, - requestId: req.id, - }); - - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - - // Archiver un projet - archiveProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.user) - return new Unauthorized401('Cannot find requestor in database'); - if (!perms.projectPermissions && !isAdmin) return new NotFound404(); - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); - - const body = await archiveProject(projectId, perms.user, req.id); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - // Récupérer les données de tous les projets pour export - getProjectsData: async ({ request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await generateProjectsData(); - - return { - status: 200, - body, - }; - }, - - bulkActionProject: async ({ request: req, body }) => { - const perms = await authUser(req); - - if (!perms.user) - return new Unauthorized401('Cannot find requestor in database'); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - await bulkActionProject(body, perms.user, req.id); - - return { - status: 202, - body: null, - }; - }, - }); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + await bulkActionProject(body, perms.user, req.id); + + return { + status: 202, + body: null, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts index 70352d135..035ddc1a1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts @@ -4,7 +4,8 @@ import { fakeToken, repositoryContract, } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, @@ -22,170 +23,175 @@ import { updateRepository, } from './business.js'; -export function repositoryRouter() { - return serverInstance.router(repositoryContract, { - // Récupérer tous les repositories d'un projet - listRepositories: async ({ request: req, query }) => { - const projectId = query.projectId; - const perms = await authUser(req, { id: projectId }); - - const body = ProjectAuthorized.ListRepositories(perms) - ? await getProjectRepositories(projectId) - : []; - - return { - status: 200, - body, - }; - }, - - // Synchroniser un repository - syncRepository: async ({ request: req, params, body }) => { - const { repositoryId } = params; - const perms = await authUser(req, { repositoryId }); - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const { syncAllBranches, branchName } = body; - - const resBody = await syncRepository({ - repositoryId, - userId: perms.user.id, - branchName, - requestId: req.id, - syncAllBranches, - }); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 204, - body: resBody, - }; - }, - - // Créer un repository - createRepository: async ({ request: req, body: data }) => { - const projectId = data.projectId; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await createRepository({ - data, - userId: perms.user.id, - requestId: req.id, - }); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Mettre à jour un repository - updateRepository: async ({ request: req, params, body }) => { - const repositoryId = params.repositoryId; - const perms = await authUser(req, { repositoryId }); - - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const keysAllowedForUpdate = [ - 'externalRepoUrl', - 'isPrivate', - 'externalToken', - 'externalUserName', - 'isInfra', - ]; - const data = filterObjectByKeys(body, keysAllowedForUpdate); - - if (data.externalToken === fakeToken) { - delete data.externalToken; - } - - if (data.isPrivate === false) { - delete data.externalToken; - delete data.externalUserName; - } - - const resBody = await updateRepository({ - repositoryId, - data, - userId: perms.user.id, - requestId: req.id, - }); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - // Supprimer un repository - deleteRepository: async ({ request: req, params }) => { - const repositoryId = params.repositoryId; - const perms = await authUser(req, { repositoryId }); - - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await deleteRepository({ - repositoryId, - userId: perms.user.id, - requestId: req.id, - projectId: perms.projectId, - }); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); +@Injectable() +export class RepositoryRouterService { + constructor(private readonly appService: AppService) {} + + repositoryRouter() { + return this.appService.serverInstance.router(repositoryContract, { + // Récupérer tous les repositories d'un projet + listRepositories: async ({ request: req, query }) => { + const projectId = query.projectId; + const perms = await authUser(req, { id: projectId }); + + const body = ProjectAuthorized.ListRepositories(perms) + ? await getProjectRepositories(projectId) + : []; + + return { + status: 200, + body, + }; + }, + + // Synchroniser un repository + syncRepository: async ({ request: req, params, body }) => { + const { repositoryId } = params; + const perms = await authUser(req, { repositoryId }); + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const { syncAllBranches, branchName } = body; + + const resBody = await syncRepository({ + repositoryId, + userId: perms.user.id, + branchName, + requestId: req.id, + syncAllBranches, + }); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 204, + body: resBody, + }; + }, + + // Créer un repository + createRepository: async ({ request: req, body: data }) => { + const projectId = data.projectId; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await createRepository({ + data, + userId: perms.user.id, + requestId: req.id, + }); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Mettre à jour un repository + updateRepository: async ({ request: req, params, body }) => { + const repositoryId = params.repositoryId; + const perms = await authUser(req, { repositoryId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const keysAllowedForUpdate = [ + 'externalRepoUrl', + 'isPrivate', + 'externalToken', + 'externalUserName', + 'isInfra', + ]; + const data = filterObjectByKeys(body, keysAllowedForUpdate); + + if (data.externalToken === fakeToken) { + delete data.externalToken; + } + + if (data.isPrivate === false) { + delete data.externalToken; + delete data.externalUserName; + } + + const resBody = await updateRepository({ + repositoryId, + data, + userId: perms.user.id, + requestId: req.id, + }); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 200, + body: resBody, + }; + }, + + // Supprimer un repository + deleteRepository: async ({ request: req, params }) => { + const repositoryId = params.repositoryId; + const perms = await authUser(req, { repositoryId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await deleteRepository({ + repositoryId, + userId: perms.user.id, + requestId: req.id, + projectId: perms.projectId, + }); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts index aec3ea1e4..0c8634cba 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts @@ -1,6 +1,7 @@ import type { AsyncReturnType } from '@cpn-console/shared'; import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import '@old-server/types/index.js'; import { authUser } from '@old-server/utils/controller.js'; import { Forbidden403 } from '@old-server/utils/errors.js'; @@ -13,78 +14,84 @@ import { validateServiceChain as validateServiceChainBusiness, } from './business.js'; -export function serviceChainRouter() { - return serverInstance.router(serviceChainContract, { - listServiceChains: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - - let body: AsyncReturnType = []; - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listServiceChainsBusiness(); - } - - return { - status: 200, - body, - }; - }, - - getServiceChainDetails: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - const serviceChainDetails = - await getServiceChainDetailsBusiness(serviceChainId); - - return { - status: 200, - body: serviceChainDetails, - }; - }, - - retryServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - await retryServiceChainBusiness(serviceChainId); - - return { - status: 204, - body: null, - }; - }, - - validateServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.validationId; - await validateServiceChainBusiness(serviceChainId); - - return { - status: 204, - body: null, - }; - }, - - getServiceChainFlows: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - const serviceChainFlows = - await getServiceChainFlowsBusiness(serviceChainId); - - return { - status: 200, - body: serviceChainFlows, - }; - }, - }); +@Injectable() +export class ServiceChainRouterService { + constructor(private readonly appService: AppService) {} + + serviceChainRouter() { + return this.appService.serverInstance.router(serviceChainContract, { + listServiceChains: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + + let body: AsyncReturnType = + []; + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listServiceChainsBusiness(); + } + + return { + status: 200, + body, + }; + }, + + getServiceChainDetails: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + const serviceChainDetails = + await getServiceChainDetailsBusiness(serviceChainId); + + return { + status: 200, + body: serviceChainDetails, + }; + }, + + retryServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + await retryServiceChainBusiness(serviceChainId); + + return { + status: 204, + body: null, + }; + }, + + validateServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.validationId; + await validateServiceChainBusiness(serviceChainId); + + return { + status: 204, + body: null, + }; + }, + + getServiceChainFlows: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + const serviceChainFlows = + await getServiceChainFlowsBusiness(serviceChainId); + + return { + status: 200, + body: serviceChainFlows, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts index 02347ad14..cba22d5e8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts @@ -1,46 +1,52 @@ import { AdminAuthorized, serviceContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { Forbidden403 } from '@old-server/utils/errors.js'; import { checkServicesHealth, refreshServicesHealth } from './business.js'; -export function serviceMonitorRouter() { - return serverInstance.router(serviceContract, { - getServiceHealth: async () => { - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - - getCompleteServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - - refreshServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - - await refreshServicesHealth(); - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - }); +@Injectable() +export class ServiceMonitorRouterService { + constructor(private readonly appService: AppService) {} + + serviceMonitorRouter() { + return this.appService.serverInstance.router(serviceContract, { + getServiceHealth: async () => { + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + + getCompleteServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + + refreshServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + + await refreshServicesHealth(); + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts index 07ec2a55a..4a6c7dc27 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts @@ -1,5 +1,6 @@ import { AdminAuthorized, stageContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; @@ -11,81 +12,86 @@ import { updateStage, } from './business.js'; -export function stageRouter() { - return serverInstance.router(stageContract, { - // Récupérer les types d'environnement disponibles - listStages: async () => { - const body = await listStages(); - - return { - status: 200, - body, - }; - }, - - // Récupérer les environnements associés au stage - getStageEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - const body = await getStageAssociatedEnvironments(stageId); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Créer un stage - createStage: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const body = await createStage(data); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Modifier une association stage / clusters - updateStage: async ({ request: req, params, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - - const body = await updateStage(stageId, data); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Supprimer un stage - deleteStage: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - - const body = await deleteStage(stageId); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); +@Injectable() +export class StageRouterService { + constructor(private readonly appService: AppService) {} + + stageRouter() { + return this.appService.serverInstance.router(stageContract, { + // Récupérer les types d'environnement disponibles + listStages: async () => { + const body = await listStages(); + + return { + status: 200, + body, + }; + }, + + // Récupérer les environnements associés au stage + getStageEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + const body = await getStageAssociatedEnvironments(stageId); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Créer un stage + createStage: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const body = await createStage(data); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Modifier une association stage / clusters + updateStage: async ({ request: req, params, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + + const body = await updateStage(stageId, data); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Supprimer un stage + deleteStage: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + + const body = await deleteStage(stageId); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts index d7a2218f5..2237e5f82 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts @@ -1,38 +1,44 @@ import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; import { getPluginsConfig, updatePluginConfig } from './business.js'; -export function pluginConfigRouter() { - return serverInstance.router(systemPluginContract, { - // Récupérer les configurations plugins - getPluginsConfig: async ({ request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); +@Injectable() +export class SystemConfigRouterService { + constructor(private readonly appService: AppService) {} - const services = await getPluginsConfig(); + pluginConfigRouter() { + return this.appService.serverInstance.router(systemPluginContract, { + // Récupérer les configurations plugins + getPluginsConfig: async ({ request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - return { - status: 200, - body: services, - }; - }, - // Mettre à jour les configurations plugins - updatePluginsConfig: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); + const services = await getPluginsConfig(); - const resBody = await updatePluginConfig(body); - if (resBody instanceof ErrorResType) return resBody; + return { + status: 200, + body: services, + }; + }, + // Mettre à jour les configurations plugins + updatePluginsConfig: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - return { - status: 204, - body: resBody, - }; - }, - }); + const resBody = await updatePluginConfig(body); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 204, + body: resBody, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts index 64dacc7c9..c53ef511f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts @@ -1,21 +1,27 @@ import { systemContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { appVersion } from '@old-server/utils/env.js'; -export function systemRouter() { - return serverInstance.router(systemContract, { - getVersion: async () => ({ - status: 200, - body: { - version: appVersion, - }, - }), +@Injectable() +export class SystemRouterService { + constructor(private readonly appService: AppService) {} - getHealth: async () => ({ - status: 200, - body: { - status: 'OK', - }, - }), - }); + systemRouter() { + return this.appService.serverInstance.router(systemContract, { + getVersion: async () => ({ + status: 200, + body: { + version: appVersion, + }, + }), + + getHealth: async () => ({ + status: 200, + body: { + status: 'OK', + }, + }), + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts index 99a094c01..fa43afb34 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts @@ -1,32 +1,38 @@ import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { Forbidden403 } from '@old-server/utils/errors.js'; import { getSystemSettings, upsertSystemSetting } from './business.js'; -export function systemSettingsRouter() { - return serverInstance.router(systemSettingsContract, { - listSystemSettings: async ({ query }) => { - const systemSettings = await getSystemSettings(query.key); +@Injectable() +export class SystemSettingsRouterService { + constructor(private readonly appService: AppService) {} - return { - status: 200, - body: systemSettings, - }; - }, + systemSettingsRouter() { + return this.appService.serverInstance.router(systemSettingsContract, { + listSystemSettings: async ({ query }) => { + const systemSettings = await getSystemSettings(query.key); - upsertSystemSetting: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); + return { + status: 200, + body: systemSettings, + }; + }, - const systemSetting = await upsertSystemSetting(data); + upsertSystemSetting: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - return { - status: 201, - body: systemSetting, - }; - }, - }); + const systemSetting = await upsertSystemSetting(data); + + return { + status: 201, + body: systemSetting, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts index 4d3c146bf..afea7a061 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts @@ -1,5 +1,6 @@ import { AdminAuthorized, userContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import '@old-server/types/index.js'; import { authUser } from '@old-server/utils/controller.js'; import { @@ -15,59 +16,64 @@ import { patchUsers, } from './business.js'; -export function userRouter() { - return serverInstance.router(userContract, { - getMatchingUsers: async ({ query }) => { - const usersMatching = await getMatchingUsers(query); +@Injectable() +export class UserRouterService { + constructor(private readonly appService: AppService) {} - return { - status: 200, - body: usersMatching, - }; - }, + userRouter() { + return this.appService.serverInstance.router(userContract, { + getMatchingUsers: async ({ query }) => { + const usersMatching = await getMatchingUsers(query); - auth: async ({ request: req }) => { - const user = req.session.user; + return { + status: 200, + body: usersMatching, + }; + }, - if (!user) return new Unauthorized401(); + auth: async ({ request: req }) => { + const user = req.session.user; - const { user: body } = await logViaSession(user); + if (!user) return new Unauthorized401(); - return { - status: 200, - body, - }; - }, + const { user: body } = await logViaSession(user); - getAllUsers: async ({ - request: req, - query: { relationType, ...query }, - }) => { - const perms = await authUser(req); + return { + status: 200, + body, + }; + }, - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); + getAllUsers: async ({ + request: req, + query: { relationType, ...query }, + }) => { + const perms = await authUser(req); - const body = await getUsers(query, relationType); - if (body instanceof ErrorResType) return body; + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - return { - status: 200, - body, - }; - }, + const body = await getUsers(query, relationType); + if (body instanceof ErrorResType) return body; - patchUsers: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); + return { + status: 200, + body, + }; + }, - const users = await patchUsers(body); + patchUsers: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); - return { - status: 200, - body: users, - }; - }, - }); + const users = await patchUsers(body); + + return { + status: 200, + body: users, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts index 0d178b3d4..e94defcf3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts @@ -1,51 +1,63 @@ import { personalAccessTokenContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import '@old-server/types/index.js'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; import { createToken, deleteToken, listTokens } from './business.js'; -export function personalAccessTokenRouter() { - return serverInstance.router(personalAccessTokenContract, { - listPersonalAccessTokens: async ({ request: req }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - const body = await listTokens(perms.user.id); - - return { - status: 200, - body, - }; - }, - - createPersonalAccessToken: async ({ request: req, body: data }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - const body = await createToken(data, perms.user.id); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - deletePersonalAccessToken: async ({ request: req, params }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - await deleteToken(params.tokenId, perms.user.id); - - return { - status: 204, - body: null, - }; - }, - }); +@Injectable() +export class UserTokensRouterService { + constructor(private readonly appService: AppService) {} + + personalAccessTokenRouter() { + return this.appService.serverInstance.router( + personalAccessTokenContract, + { + listPersonalAccessTokens: async ({ request: req }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + const body = await listTokens(perms.user.id); + + return { + status: 200, + body, + }; + }, + + createPersonalAccessToken: async ({ + request: req, + body: data, + }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + const body = await createToken(data, perms.user.id); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + deletePersonalAccessToken: async ({ request: req, params }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + await deleteToken(params.tokenId, perms.user.id); + + return { + status: 204, + body: null, + }; + }, + }, + ); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts index f0bf94824..d9c158e20 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts @@ -1,5 +1,6 @@ import { AdminAuthorized, zoneContract } from '@cpn-console/shared'; -import { serverInstance } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app.js'; import { authUser } from '@old-server/utils/controller.js'; import { ErrorResType, @@ -9,72 +10,77 @@ import { import { createZone, deleteZone, listZones, updateZone } from './business.js'; -export function zoneRouter() { - return serverInstance.router(zoneContract, { - listZones: async () => { - const zones = await listZones(); +@Injectable() +export class ZoneRouterService { + constructor(private readonly appService: AppService) {} - return { - status: 200, - body: zones, - }; - }, + zoneRouter() { + return this.appService.serverInstance.router(zoneContract, { + listZones: async () => { + const zones = await listZones(); - createZone: async ({ request: req, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); + return { + status: 200, + body: zones, + }; + }, - const body = await createZone(data, user.id, req.id); - if (body instanceof ErrorResType) return body; + createZone: async ({ request: req, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); - return { - status: 201, - body, - }; - }, + const body = await createZone(data, user.id, req.id); + if (body instanceof ErrorResType) return body; - updateZone: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); + return { + status: 201, + body, + }; + }, - const zoneId = params.zoneId; + updateZone: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); - const body = await updateZone(zoneId, data, user.id, req.id); - if (body instanceof ErrorResType) return body; + const zoneId = params.zoneId; - return { - status: 200, - body, - }; - }, + const body = await updateZone(zoneId, data, user.id, req.id); + if (body instanceof ErrorResType) return body; - deleteZone: async ({ request: req, params }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - const zoneId = params.zoneId; + return { + status: 200, + body, + }; + }, - const body = await deleteZone(zoneId, user.id, req.id); - if (body instanceof ErrorResType) return body; + deleteZone: async ({ request: req, params }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + const zoneId = params.zoneId; - return { - status: 204, - body, - }; - }, - }); + const body = await deleteZone(zoneId, user.id, req.id); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts index 4f92206dc..466b80b65 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts @@ -13,8 +13,8 @@ import keycloak from 'fastify-keycloak-adapter'; import { AppService } from './app'; import { ConnectionService } from './connect'; -import { getPreparedApp } from './prepare-app'; -import { apiRouter } from './resources/index.js'; +import { PrepareAppService } from './prepare-app'; +import { ResourcesService } from './resources/index.js'; import { isCI, isDev, @@ -24,16 +24,20 @@ import { isTest, port, } from './utils/env.js'; -import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify.js'; +import { FastifyService } from './utils/fastify.js'; import { keycloakConf, sessionConf } from './utils/keycloak.js'; import type { CustomLogger } from './utils/logger.js'; -import { log } from './utils/logger.js'; +import { LoggerService } from './utils/logger.js'; @Injectable() export class ServerService { constructor( - private readonly connectionService: ConnectionService, private readonly appService: AppService, + private readonly connectionService: ConnectionService, + private readonly fastifyService: FastifyService, + private readonly loggerService: LoggerService, + private readonly prepareAppService: PrepareAppService, + private readonly resourceService: ResourcesService, ) {} app: any; @@ -72,7 +76,7 @@ export class ServerService { } async getApp(): Promise { - const app = await getPreparedApp(); + const app = await this.prepareAppService.getPreparedApp(); try { await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }); @@ -94,13 +98,13 @@ export class ServerService { async createApp() { const openApiDocument = generateOpenApi( await getContract(), - swaggerConf, + this.fastifyService.swaggerConf, { setOperationId: true, }, ); - const app = fastify(fastifyConf) + const app = fastify(this.fastifyService.fastifyConf) .register(helmet, () => ({ contentSecurityPolicy: !(isInt || isDev || isTest), })) @@ -111,8 +115,8 @@ export class ServerService { .register(fastifySwagger, { transformObject: () => openApiDocument, }) - .register(fastifySwaggerUi, swaggerUiConf) - .register(apiRouter()) + .register(fastifySwaggerUi, this.fastifyService.swaggerConf as any) + .register(this.resourceService.apiRouter()) .addHook('onRoute', (opts) => { if (opts.path === `${apiPrefix}/healthz`) { opts.logLevel = 'silent'; @@ -127,7 +131,7 @@ export class ServerService { error: message, stack: error.stack, }); - log('info', { reqId: req.id, error }); + this.loggerService.log('info', { reqId: req.id, error }); }) .addHook('onResponse', (req, res) => { if (res.statusCode < 400) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts index 100a2b166..7fdca91b0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts @@ -1,5 +1,6 @@ import { swaggerUiPath } from '@cpn-console/shared'; import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui'; +import { Injectable } from '@nestjs/common'; import type { generateOpenApi } from '@ts-rest/open-api'; import type { FastifyServerOptions } from 'fastify'; import { randomUUID } from 'node:crypto'; @@ -12,45 +13,54 @@ import { keycloakRealm, keycloakRedirectUri, } from './env.js'; -import { loggerConf } from './logger.js'; +import { LoggerService } from './logger.js'; -export const fastifyConf: FastifyServerOptions = { - maxParamLength: 5000, - logger: loggerConf[NODE_ENV] ?? loggerConf.production, - genReqId: () => randomUUID(), -}; +@Injectable() +export class FastifyService { + constructor(private readonly loggerService: LoggerService) { + this.fastifyConf = { + maxParamLength: 5000, + logger: + this.loggerService.loggerConf[NODE_ENV] ?? + this.loggerService.loggerConf.production, + genReqId: () => randomUUID(), + }; + } -const externalDocs = { - description: 'External documentation.', - url: 'https://cloud-pi-native.fr', -}; + fastifyConf!: FastifyServerOptions; -export const swaggerConf: Parameters[1] = { - info: { - title: 'Console Cloud Pi Native', - description: 'API de gestion des ressources Cloud Pi Native.', - version: appVersion, - }, + externalDocs = { + description: 'External documentation.', + url: 'https://cloud-pi-native.fr', + }; - externalDocs, - servers: [ - { - url: keycloakRedirectUri, + swaggerConf: Parameters[1] = { + info: { + title: 'Console Cloud Pi Native', + description: 'API de gestion des ressources Cloud Pi Native.', + version: appVersion, }, - ], -}; -export const swaggerUiConf: FastifySwaggerUiOptions = { - routePrefix: swaggerUiPath, - uiConfig: { - docExpansion: 'list', - deepLinking: false, - }, - initOAuth: { - clientId: keycloakClientId, - clientSecret: keycloakClientSecret, - realm: keycloakRealm, - appName: 'Cloud Pi Native', - scopes: 'openid generic', - }, -}; + externalDocs: this.externalDocs, + servers: [ + { + url: keycloakRedirectUri, + }, + ], + }; + + swaggerUiConf: FastifySwaggerUiOptions = { + routePrefix: swaggerUiPath, + uiConfig: { + docExpansion: 'list', + deepLinking: false, + }, + initOAuth: { + clientId: keycloakClientId, + clientSecret: keycloakClientSecret, + realm: keycloakRealm, + appName: 'Cloud Pi Native', + scopes: 'openid generic', + }, + }; +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts index cdadb61a0..86243f823 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts @@ -1,37 +1,11 @@ import type { XOR } from '@cpn-console/shared'; -import { logger as customLogger } from '@old-server/app.js'; +import { Injectable } from '@nestjs/common'; +import { AppService } from '@old-server/app'; import type { FastifyBaseLogger, FastifyLogFn, PinoLoggerOptions, -} from 'fastify/types/logger.js'; - -export const customLevels = { - audit: 25, -}; - -export const loggerConf: Record = { - development: { - transport: { - target: 'pino-pretty', - options: { - translateTime: 'dd/mm/yyyy - HH:MM:ss Z', - ignore: 'pid,hostname', - colorize: true, - singleLine: true, - }, - }, - customLevels, - level: process.env.LOG_LEVEL ?? 'debug', - }, - production: { - customLevels, - level: process.env.LOG_LEVEL ?? 'audit', - }, - test: { - level: 'silent', - }, -}; +} from 'fastify/types/logger'; type LoggerType = | 'info' @@ -42,64 +16,6 @@ type LoggerType = | 'debug' | 'audit' | undefined; -const loggerWrapper = { - level: '', - child: () => loggerWrapper, - silent: () => {}, - audit: (msg: string | unknown) => console.log(msg), - info: (msg: string | unknown) => console.log(msg), - warn: (msg: string | unknown) => console.warn(msg), - error: (msg: string | unknown) => console.error(msg), - fatal: (msg: string | unknown) => console.error(msg), - trace: (msg: string | unknown) => console.trace(msg), - debug: (msg: string | unknown) => console.debug(msg), -}; - -export function log( - type: LoggerType, - { - reqId, - userId, - tokenId, - message, - error, - infos, - }: { - reqId?: string; - userId?: string; - tokenId?: string; - infos?: Record; - } & XOR< - { message: string }, - { error: Record | string | Error } - >, -) { - const logger = customLogger || loggerWrapper; - - const logInfos = { - message, - infos, - reqId, - userId, - tokenId, - }; - - if (error) { - const errorInfos = { - ...logInfos, - error: { - message: - typeof error === 'string' - ? error - : error?.message || 'unexpected error', - trace: error instanceof Error && error?.stack, - }, - }; - logger.error({ ...errorInfos }); - return; - } - logger[type || 'info']({ reqId, userId, logInfos }); -} export interface CustomLogger extends FastifyBaseLogger { /** @@ -113,3 +29,94 @@ export interface CustomLogger extends FastifyBaseLogger { */ audit: FastifyLogFn; } + +@Injectable() +export class LoggerService { + constructor(private readonly appService: AppService) {} + + customLevels = { + audit: 25, + }; + + loggerConf: Record = { + development: { + transport: { + target: 'pino-pretty', + options: { + translateTime: 'dd/mm/yyyy - HH:MM:ss Z', + ignore: 'pid,hostname', + colorize: true, + singleLine: true, + }, + }, + customLevels: this.customLevels, + level: process.env.LOG_LEVEL ?? 'debug', + }, + production: { + customLevels: this.customLevels, + level: process.env.LOG_LEVEL ?? 'audit', + }, + test: { + level: 'silent', + }, + }; + + loggerWrapper = { + level: '', + child: () => this.loggerWrapper, + silent: () => {}, + audit: (msg: string | unknown) => console.log(msg), + info: (msg: string | unknown) => console.log(msg), + warn: (msg: string | unknown) => console.warn(msg), + error: (msg: string | unknown) => console.error(msg), + fatal: (msg: string | unknown) => console.error(msg), + trace: (msg: string | unknown) => console.trace(msg), + debug: (msg: string | unknown) => console.debug(msg), + }; + + log( + type: LoggerType, + { + reqId, + userId, + tokenId, + message, + error, + infos, + }: { + reqId?: string; + userId?: string; + tokenId?: string; + infos?: Record; + } & XOR< + { message: string }, + { error: Record | string | Error } + >, + ) { + const logger = this.appService.logger || this.loggerWrapper; + + const logInfos = { + message, + infos, + reqId, + userId, + tokenId, + }; + + if (error) { + const errorInfos = { + ...logInfos, + error: { + message: + typeof error === 'string' + ? error + : error?.message || 'unexpected error', + trace: error instanceof Error && error?.stack, + }, + }; + logger.error({ ...errorInfos }); + return; + } + logger[type || 'info']({ reqId, userId, logInfos }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/tsconfig.json b/apps/server-nestjs/src/cpin-module/old-server/tsconfig.json deleted file mode 100644 index a3397898c..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": [ - "@cpn-console/ts-config/tsconfig.base.json" - ], - "compilerOptions": { - "baseUrl": "./", - "rootDir": "./src", - "paths": { - "@/*": ["src/*"] - }, - "useUnknownInCatchVariables": false, - "declarationDir": "./types", - "outDir": "./dist", - "plugins": [{ "transform": "typescript-transform-paths" }] - }, - "include": [ - "./src/**/*.ts", - "./src/**/*.js" - ], - "exclude": [ - "./src/**/*.spec.ts", - "./src/**/__mocks__", - "./src/mocks/utils.ts", - "./src/utils/mocks.ts" - ] -} From 0c3cc3cd6c91733927d9be419e3a4133c879f274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 9 Dec 2025 15:12:34 +0100 Subject: [PATCH 07/33] chore: convert plugin manager to NestJS service --- .../src/cpin-module/old-server/src/plugins.ts | 84 ++++++++++--------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts index 7ec0df5b0..5d75de9d1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts @@ -7,50 +7,54 @@ import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin'; import { plugin as nexus } from '@cpn-console/nexus-plugin'; import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin'; import { plugin as vault } from '@cpn-console/vault-plugin'; +import { Injectable } from '@nestjs/common'; import { readdirSync, statSync } from 'node:fs'; -import { pluginsDir } from './utils/env.js'; -import { pluginManagerOptions } from './utils/plugins.js'; +import { pluginsDir } from './utils/env'; +import { pluginManagerOptions } from './utils/plugins'; -export async function initPm() { - const pm = pluginManager(pluginManagerOptions); - pm.register(argo); - pm.register(gitlab); - pm.register(harbor); - pm.register(keycloak); - pm.register(kubernetes); - pm.register(nexus); - pm.register(sonarqube); - pm.register(vault); +@Injectable() +export class PluginService { + async initPm() { + const pm = pluginManager(pluginManagerOptions); + pm.register(argo); + pm.register(gitlab); + pm.register(harbor); + pm.register(keycloak); + pm.register(kubernetes); + pm.register(nexus); + pm.register(sonarqube); + pm.register(vault); - if ( - !statSync(pluginsDir, { - throwIfNoEntry: false, - }) - ) { - return pm; - } - for (const dirName of readdirSync(pluginsDir)) { - const moduleAbsPath = `${pluginsDir}/${dirName}`; - try { - statSync(`${moduleAbsPath}/package.json`); - const pkg = await import(`${moduleAbsPath}/package.json`, { - with: { type: 'json' }, - }); - const entrypoint = pkg.default.module || pkg.default.main; - if (!entrypoint) - throw new Error( - `No entrypoint found in package.json : ${pkg.default.name}`, - ); - const { plugin } = (await import( - `${moduleAbsPath}/${entrypoint}` - )) as { plugin: Plugin }; - pm.register(plugin); - } catch (error) { - console.error(`Could not import module ${moduleAbsPath}`); - console.error(error.stack); + if ( + !statSync(pluginsDir, { + throwIfNoEntry: false, + }) + ) { + return pm; + } + for (const dirName of readdirSync(pluginsDir)) { + const moduleAbsPath = `${pluginsDir}/${dirName}`; + try { + statSync(`${moduleAbsPath}/package.json`); + const pkg = await import(`${moduleAbsPath}/package.json`, { + with: { type: 'json' }, + }); + const entrypoint = pkg.default.module || pkg.default.main; + if (!entrypoint) + throw new Error( + `No entrypoint found in package.json : ${pkg.default.name}`, + ); + const { plugin } = (await import( + `${moduleAbsPath}/${entrypoint}` + )) as { plugin: Plugin }; + pm.register(plugin); + } catch (error) { + console.error(`Could not import module ${moduleAbsPath}`); + console.error(error.stack); + } } - } - return pm; + return pm; + } } From f72614504bbfe567add16eeba2bfc21a0caf2cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 9 Dec 2025 15:13:13 +0100 Subject: [PATCH 08/33] chore: remove .js extensions from imports --- .../old-server/src/__mocks__/prisma.ts | 2 +- .../src/__mocks__/utils/hook-wrapper.ts | 2 +- .../cpin-module/old-server/src/app.spec.ts | 6 ++-- .../src/cpin-module/old-server/src/app.ts | 12 ++++---- .../old-server/src/connect.spec.ts | 30 +++++++++---------- .../src/cpin-module/old-server/src/connect.ts | 6 ++-- .../old-server/src/init/db/dump.ts | 6 ++-- .../old-server/src/init/db/index.ts | 6 ++-- .../old-server/src/init/db/utils.spec.ts | 4 +-- .../old-server/src/mocks/prisma.ts | 2 +- .../old-server/src/prepare-app.spec.ts | 18 +++++------ .../cpin-module/old-server/src/prepare-app.ts | 16 +++++----- .../src/resources/admin-role/business.spec.ts | 6 ++-- .../src/resources/admin-role/business.ts | 8 ++--- .../src/resources/admin-role/queries.ts | 2 +- .../src/resources/admin-role/router.spec.ts | 12 ++++---- .../src/resources/admin-role/router.ts | 8 ++--- .../resources/admin-token/business.spec.ts | 4 +-- .../src/resources/admin-token/business.ts | 4 +-- .../src/resources/admin-token/router.spec.ts | 12 ++++---- .../src/resources/admin-token/router.ts | 8 ++--- .../src/resources/cluster/business.spec.ts | 10 +++---- .../src/resources/cluster/business.ts | 14 ++++----- .../src/resources/cluster/queries.ts | 2 +- .../src/resources/cluster/router.spec.ts | 12 ++++---- .../src/resources/cluster/router.ts | 10 +++---- .../resources/environment/business.spec.ts | 10 +++---- .../src/resources/environment/business.ts | 10 +++---- .../src/resources/environment/queries.ts | 2 +- .../src/resources/environment/router.spec.ts | 10 +++---- .../src/resources/environment/router.ts | 8 ++--- .../src/resources/log/business.spec.ts | 4 +-- .../old-server/src/resources/log/business.ts | 2 +- .../old-server/src/resources/log/queries.ts | 2 +- .../src/resources/log/router.spec.ts | 10 +++---- .../old-server/src/resources/log/router.ts | 10 +++---- .../src/resources/project-member/business.ts | 10 +++---- .../src/resources/project-member/queries.ts | 2 +- .../resources/project-member/router.spec.ts | 12 ++++---- .../src/resources/project-member/router.ts | 8 ++--- .../resources/project-role/business.spec.ts | 6 ++-- .../src/resources/project-role/business.ts | 6 ++-- .../src/resources/project-role/queries.ts | 2 +- .../src/resources/project-role/router.spec.ts | 14 ++++----- .../src/resources/project-role/router.ts | 8 ++--- .../src/resources/project-service/business.ts | 2 +- .../src/resources/project-service/queries.ts | 4 +-- .../resources/project-service/router.spec.ts | 10 +++---- .../src/resources/project-service/router.ts | 8 ++--- .../src/resources/project/business.spec.ts | 14 ++++----- .../src/resources/project/business.ts | 16 +++++----- .../src/resources/project/queries.ts | 6 ++-- .../src/resources/project/router.spec.ts | 14 ++++----- .../src/resources/project/router.ts | 8 ++--- .../old-server/src/resources/queries-index.ts | 28 ++++++++--------- .../src/resources/repository/business.ts | 6 ++-- .../src/resources/repository/queries.ts | 2 +- .../src/resources/repository/router.spec.ts | 12 ++++---- .../src/resources/repository/router.ts | 10 +++---- .../resources/service-chain/business.spec.ts | 2 +- .../src/resources/service-chain/business.ts | 2 +- .../resources/service-chain/router.spec.ts | 10 +++---- .../src/resources/service-chain/router.ts | 10 +++---- .../resources/service-monitor/router.spec.ts | 10 +++---- .../src/resources/service-monitor/router.ts | 8 ++--- .../src/resources/stage/business.spec.ts | 6 ++-- .../src/resources/stage/business.ts | 6 ++-- .../old-server/src/resources/stage/queries.ts | 2 +- .../src/resources/stage/router.spec.ts | 12 ++++---- .../old-server/src/resources/stage/router.ts | 8 ++--- .../resources/system/config/business.spec.ts | 4 +-- .../src/resources/system/config/business.ts | 4 +-- .../src/resources/system/config/queries.ts | 4 +-- .../resources/system/config/router.spec.ts | 12 ++++---- .../src/resources/system/config/router.ts | 8 ++--- .../old-server/src/resources/system/index.ts | 2 +- .../src/resources/system/router.spec.ts | 4 +-- .../old-server/src/resources/system/router.ts | 4 +-- .../src/resources/system/settings/business.ts | 2 +- .../src/resources/system/settings/queries.ts | 2 +- .../resources/system/settings/router.spec.ts | 10 +++---- .../src/resources/system/settings/router.ts | 8 ++--- .../src/resources/user/business.spec.ts | 8 ++--- .../old-server/src/resources/user/business.ts | 8 ++--- .../old-server/src/resources/user/queries.ts | 2 +- .../src/resources/user/router.spec.ts | 10 +++---- .../old-server/src/resources/user/router.ts | 10 +++---- .../src/resources/user/tokens/business.ts | 4 +-- .../src/resources/user/tokens/router.ts | 10 +++---- .../src/resources/zone/business.spec.ts | 12 ++++---- .../old-server/src/resources/zone/business.ts | 10 +++---- .../old-server/src/resources/zone/queries.ts | 2 +- .../src/resources/zone/router.spec.ts | 12 ++++---- .../old-server/src/resources/zone/router.ts | 8 ++--- .../cpin-module/old-server/src/server.spec.ts | 14 ++++----- .../src/cpin-module/old-server/src/server.ts | 12 ++++---- .../old-server/src/utils/business.ts | 2 +- .../old-server/src/utils/controller.ts | 10 +++---- .../old-server/src/utils/date.spec.ts | 2 +- .../old-server/src/utils/fastify.ts | 4 +-- .../old-server/src/utils/hook-wrapper.spec.ts | 4 +-- .../old-server/src/utils/hook-wrapper.ts | 8 ++--- .../src/utils/keycloak-utils.spec.ts | 2 +- .../old-server/src/utils/keycloak.ts | 4 +-- .../cpin-module/old-server/src/utils/mocks.ts | 4 +-- .../old-server/src/utils/plugins.ts | 2 +- .../old-server/src/utils/proxy.spec.ts | 2 +- .../src/utils/queries-tools.spec.ts | 2 +- .../src/cpin-module/old-server/vite.config.ts | 2 +- .../cpin-module/old-server/vitest.config.ts | 10 +++---- 110 files changed, 410 insertions(+), 408 deletions(-) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts index 9c88b20e7..265c128bc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts @@ -2,7 +2,7 @@ import type { PrismaClient } from '@prisma/client'; import { beforeEach, vi } from 'vitest'; import { mockDeep, mockReset } from 'vitest-mock-extended'; -vi.mock('../prisma.js'); +vi.mock('../prisma'); const prisma = mockDeep(); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts index ac35e073d..e9fa3359d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts @@ -1,7 +1,7 @@ import { beforeEach, vi } from 'vitest'; import { mockDeep, mockReset } from 'vitest-mock-extended'; -vi.mock('../utils/hook-wrapper.ts'); +vi.mock('../utils/hook-wrapper'); export const hook = { cluster: { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts index 3e9b26571..081d636a6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts @@ -1,12 +1,12 @@ import { apiPrefix } from '@cpn-console/shared'; import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; -import app from './app.js'; -import { getRandomRequestor, setRequestor } from './utils/mocks.js'; +import app from './app'; +import { getRandomRequestor, setRequestor } from './utils/mocks'; vi.mock( 'fastify-keycloak-adapter', - (await import('./utils/mocks.js')).mockSessionPlugin, + (await import('./utils/mocks')).mockSessionPlugin, ); describe('app', () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts index b64a34d2b..1530ab448 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts @@ -11,12 +11,12 @@ import type { FastifyRequest } from 'fastify'; import fastify from 'fastify'; import keycloak from 'fastify-keycloak-adapter'; -import { ResourcesService } from './resources/index.js'; -import { isDev, isInt, isTest } from './utils/env.js'; -import { FastifyService } from './utils/fastify.js'; -import { keycloakConf, sessionConf } from './utils/keycloak.js'; -import type { CustomLogger } from './utils/logger.js'; -import { LoggerService } from './utils/logger.js'; +import { ResourcesService } from './resources/index'; +import { isDev, isInt, isTest } from './utils/env'; +import { FastifyService } from './utils/fastify'; +import { keycloakConf, sessionConf } from './utils/keycloak'; +import type { CustomLogger } from './utils/logger'; +import { LoggerService } from './utils/logger'; @Injectable() export class AppService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts index 9af7a8734..004275b7f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts @@ -1,24 +1,24 @@ -import { PrismaClientInitializationError } from '@prisma/client/runtime/library.js'; +import { PrismaClientInitializationError } from '@prisma/client/runtime/library'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import prisma from './__mocks__/prisma.js'; -import app, { logger } from './app.js'; -import { getConnection } from './connect.js'; +import prisma from './__mocks__/prisma'; +import app, { logger } from './app'; +import { getConnection } from './connect'; vi.mock( 'fastify-keycloak-adapter', - (await import('./utils/mocks.js')).mockSessionPlugin, + (await import('./utils/mocks')).mockSessionPlugin, ); -vi.mock('@old-server/resources/queries-index.js'); -vi.mock('./models/log.js', () => getModel('getLogModel')); -vi.mock('./models/repository.js', () => getModel('getRepositoryModel')); -vi.mock('./models/permission.js', () => getModel('getPermissionModel')); -vi.mock('./models/environment.js', () => getModel('getEnvironmentModel')); -vi.mock('./models/project.js', () => getModel('getProjectModel')); -vi.mock('./models/user.js', () => getModel('getUserModel')); -vi.mock('./models/users-projects.js', () => getModel('getRolesModel')); -vi.mock('./models/zone.js', () => getModel('getZoneModel')); -vi.mock('./prisma.js'); +vi.mock('@old-server/resources/queries-index'); +vi.mock('./models/log', () => getModel('getLogModel')); +vi.mock('./models/repository', () => getModel('getRepositoryModel')); +vi.mock('./models/permission', () => getModel('getPermissionModel')); +vi.mock('./models/environment', () => getModel('getEnvironmentModel')); +vi.mock('./models/project', () => getModel('getProjectModel')); +vi.mock('./models/user', () => getModel('getUserModel')); +vi.mock('./models/users-projects', () => getModel('getRolesModel')); +vi.mock('./models/zone', () => getModel('getZoneModel')); +vi.mock('./prisma'); vi.spyOn(app, 'listen'); vi.spyOn(logger, 'info'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts index f7d8ea5d4..acdd8f771 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { setTimeout } from 'node:timers/promises'; -import { AppService } from './app.js'; -import prisma from './prisma.js'; -import { dbUrl, isCI, isDev, isTest } from './utils/env.js'; +import { AppService } from './app'; +import prisma from './prisma'; +import { dbUrl, isCI, isDev, isTest } from './utils/env'; @Injectable() export class ConnectionService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts index 69d2bcb7b..5679aa56c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts @@ -5,7 +5,7 @@ * format ./data.ts with linter * cut/paste to packages/test-utils/src/imports/data.ts */ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { Prisma } from '@prisma/client'; import { writeFileSync } from 'node:fs'; @@ -15,7 +15,7 @@ import { modelKeys, models, resourceListToDict, -} from './utils.js'; +} from './utils'; const Models = resourceListToDict(Prisma.dmmf.datamodel.models); @@ -35,4 +35,4 @@ for (const [model, targetModel, relationKey] of manyToManyRelation) { } const a = JSON.stringify({ ...models, associations }, null, 2); -writeFileSync('./data.ts', `export const data = ${a}`); +writeFileSync('./data', `export const data = ${a}`); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts index c2e0e4112..a7b921eb4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import prisma from '@old-server/prisma.js'; +import { AppService } from '@old-server/app'; +import prisma from '@old-server/prisma'; -import { modelKeys } from './utils.js'; +import { modelKeys } from './utils'; type ExtractKeysWithFields = { [K in keyof T]: T[K] extends { fields: any } ? K : never; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts index b603e7073..50cce2c10 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { modelKeys, moveBefore, resourceListToDict } from './utils.js'; +import prisma from '../../__mocks__/prisma'; +import { modelKeys, moveBefore, resourceListToDict } from './utils'; vi.mock('fs', () => ({ writeFileSync: vi.fn() })); for (const modelKey of modelKeys) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts index 9c88b20e7..265c128bc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts @@ -2,7 +2,7 @@ import type { PrismaClient } from '@prisma/client'; import { beforeEach, vi } from 'vitest'; import { mockDeep, mockReset } from 'vitest-mock-extended'; -vi.mock('../prisma.js'); +vi.mock('../prisma'); const prisma = mockDeep(); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts index 4608568f7..7b20d355e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts @@ -1,18 +1,18 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app, { logger } from './app.js'; -import { getConnection } from './connect.js'; -import { initDb } from './init/db/index.js'; -import { getPreparedApp } from './prepare-app.js'; +import app, { logger } from './app'; +import { getConnection } from './connect'; +import { initDb } from './init/db/index'; +import { getPreparedApp } from './prepare-app'; vi.mock( 'fastify-keycloak-adapter', - (await import('./utils/mocks.js')).mockSessionPlugin, + (await import('./utils/mocks')).mockSessionPlugin, ); -vi.mock('./connect.js'); -vi.mock('./index.js'); -vi.mock('./utils/logger.js'); -vi.mock('./init/db/index.js', () => ({ initDb: vi.fn() })); +vi.mock('./connect'); +vi.mock('./index'); +vi.mock('./utils/logger'); +vi.mock('./init/db/index', () => ({ initDb: vi.fn() })); vi.spyOn(app, 'listen'); vi.spyOn(logger, 'info'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts index 465861e26..eb9f99862 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts @@ -4,7 +4,7 @@ import { ProxyAgent, setGlobalDispatcher } from 'undici'; import { AppService } from './app'; import { ConnectionService } from './connect'; import { InitDBService } from './init/db'; -import { initPm } from './plugins'; +import { PluginService } from './plugins'; import { isCI, isDev, isDevSetup, isProd, isTest } from './utils/env'; @Injectable() @@ -13,6 +13,8 @@ export class PrepareAppService { private readonly appService: AppService, private readonly connectionService: ConnectionService, private readonly initDBService: InitDBService, + private readonly pluginService: PluginService, + ) { // Workaround because fetch isn't using http_proxy variables // See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 @@ -37,15 +39,15 @@ export class PrepareAppService { throw error; } - initPm(); + this.pluginService.initPm(); this.appService.logger.info('Reading init database file'); // try { // const dataPath = // isProd || isInt - // ? './init/db/imports/data.js' - // : '@cpn-console/test-utils/src/imports/data.ts'; + // ? './init/db/imports/data' + // : '@cpn-console/test-utils/src/imports/data'; // await initializeDB(dataPath); // if (isProd && !isDevSetup) { // this.appService.logger.info('Cleaning up imported data file...'); @@ -84,15 +86,15 @@ export class PrepareAppService { throw error; } - initPm(); + this.pluginService.initPm(); this.appService.logger.info('Reading init database file'); // try { // const dataPath = // isProd || isInt - // ? './init/db/imports/data.js' - // : '@cpn-console/test-utils/src/imports/data.ts'; + // ? './init/db/imports/data' + // : '@cpn-console/test-utils/src/imports/data'; // await initializeDB(dataPath); // if (isProd && !isDevSetup) { // this.appService.logger.info('Cleaning up imported data file...'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts index 47fef06eb..94a505bc2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts @@ -2,15 +2,15 @@ import { faker } from '@faker-js/faker'; import type { AdminRole, User } from '@prisma/client'; import { describe, expect, it } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { BadRequest400 } from '../../utils/errors.ts'; +import prisma from '../../__mocks__/prisma'; +import { BadRequest400 } from '../../utils/errors'; import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles, -} from './business.ts'; +} from './business'; describe('test admin-role business', () => { describe('listRoles', () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts index 3367eb615..2198c191c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts @@ -1,8 +1,8 @@ import type { AdminRole, adminRoleContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; -import { listAdminRoles } from '@old-server/resources/queries-index.js'; -import type { ErrorResType } from '@old-server/utils/errors.js'; -import { BadRequest400 } from '@old-server/utils/errors.js'; +import prisma from '@old-server/prisma'; +import { listAdminRoles } from '@old-server/resources/queries-index'; +import type { ErrorResType } from '@old-server/utils/errors'; +import { BadRequest400 } from '@old-server/utils/errors'; import type { Project, ProjectRole } from '@prisma/client'; export async function listRoles() { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts index f09acde59..e1d5a9aec 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { AdminRole, Prisma } from '@prisma/client'; export const listAdminRoles = () => diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts index 6a9f67b59..60e5173aa 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts @@ -2,15 +2,15 @@ import { adminRoleContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; -import { getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; +import { getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessListRolesMock = vi.spyOn(business, 'listRoles'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts index ee2910b7f..9a7601d05 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts @@ -1,8 +1,8 @@ import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; import { countRolesMembers, @@ -10,7 +10,7 @@ import { deleteRole, listRoles, patchRoles, -} from './business.js'; +} from './business'; @Injectable() export class AdminRoleRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts index be45b3024..a60a38eae 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts @@ -2,8 +2,8 @@ import type { AdminToken } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { describe, expect, it } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { createToken, deleteToken, listTokens } from './business.ts'; +import prisma from '../../__mocks__/prisma'; +import { createToken, deleteToken, listTokens } from './business'; describe('test admin-token business', () => { describe('listTokens', () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts index e99f6504a..6705eb6c3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts @@ -3,11 +3,11 @@ import { generateRandomPassword, isAtLeastTomorrow, } from '@cpn-console/shared'; -import { BadRequest400 } from '@old-server/utils/errors.js'; +import { BadRequest400 } from '@old-server/utils/errors'; import type { $Enums, AdminToken, Prisma } from '@prisma/client'; import { createHash, randomUUID } from 'node:crypto'; -import prisma from '../../prisma.js'; +import prisma from '../../prisma'; export async function listTokens( query: typeof adminTokenContract.listAdminTokens.query._type, diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts index fbb3b2458..f137d1156 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts @@ -4,15 +4,15 @@ import { faker } from '@faker-js/faker'; import type { AdminToken } from '@prisma/client'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; -import { getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; +import { getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessListTokensMock = vi.spyOn(business, 'listTokens'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts index 670ce3ffa..de84c8b34 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts @@ -1,10 +1,10 @@ import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { authUser } from '@old-server/utils/controller.js'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; -import { AppService } from '../../app.js'; -import { createToken, deleteToken, listTokens } from './business.js'; +import { AppService } from '../../app'; +import { createToken, deleteToken, listTokens } from './business'; @Injectable() export class AdminTokenRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts index e1115ab87..44e8a4ab3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts @@ -2,14 +2,14 @@ import { faker } from '@faker-js/faker'; import type { Cluster, Environment } from '@prisma/client'; import { describe, expect, it, vi } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { hook } from '../../__mocks__/utils/hook-wrapper.ts'; +import prisma from '../../__mocks__/prisma'; +import { hook } from '../../__mocks__/utils/hook-wrapper'; import { BadRequest400, ErrorResType, NotFound404, Unprocessable422, -} from '../../utils/errors.ts'; +} from '../../utils/errors'; import { createCluster, deleteCluster, @@ -18,9 +18,9 @@ import { getClusterUsage, listClusters, updateCluster, -} from './business.ts'; +} from './business'; -vi.mock('../../utils/hook-wrapper.ts', async () => ({ +vi.mock('../../utils/hook-wrapper', async () => ({ hook, })); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts index 990726b21..148bb403a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts @@ -5,7 +5,7 @@ import type { clusterContract, } from '@cpn-console/shared'; import { ClusterDetailsSchema, ClusterPrivacy } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { addLogs, createCluster as createClusterQuery, @@ -22,17 +22,17 @@ import { removeClusterFromProject, removeClusterFromStage, updateCluster as updateClusterQuery, -} from '@old-server/resources/queries-index.js'; -import { linkClusterToStages } from '@old-server/resources/stage/business.js'; -import type { Resources } from '@old-server/types/index.js'; -import { validateSchema } from '@old-server/utils/business.js'; +} from '@old-server/resources/queries-index'; +import { linkClusterToStages } from '@old-server/resources/stage/business'; +import type { Resources } from '@old-server/types/index'; +import { validateSchema } from '@old-server/utils/business'; import { BadRequest400, ErrorResType, NotFound404, Unprocessable422, -} from '@old-server/utils/errors.js'; -import { hook } from '@old-server/utils/hook-wrapper.js'; +} from '@old-server/utils/errors'; +import { hook } from '@old-server/utils/hook-wrapper'; import type { Prisma, Project, User } from '@prisma/client'; export async function listClusters(userId?: User['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts index b3777acc0..ef645b3ff 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Cluster, Environment, diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts index d96e96f06..365db47a3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts @@ -3,15 +3,15 @@ import { clusterContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; -import { getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; +import { getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessListMock = vi.spyOn(business, 'listClusters'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts index 55057f70e..20870a5c2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts @@ -1,14 +1,14 @@ import type { AsyncReturnType } from '@cpn-console/shared'; import { AdminAuthorized, clusterContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import '@old-server/types/index.js'; -import { authUser } from '@old-server/utils/controller.js'; +import { AppService } from '@old-server/app'; +import '@old-server/types/index'; +import { authUser } from '@old-server/utils/controller'; import { ErrorResType, Forbidden403, Unauthorized401, -} from '@old-server/utils/errors.js'; +} from '@old-server/utils/errors'; import { createCluster, @@ -18,7 +18,7 @@ import { getClusterUsage, listClusters, updateCluster, -} from './business.js'; +} from './business'; @Injectable() export class ClusterRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts index 1d522f367..c31624ff4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts @@ -10,9 +10,9 @@ import type { } from '@prisma/client'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { hook } from '../../__mocks__/utils/hook-wrapper.ts'; -import { Result } from '../../utils/business.js'; +import prisma from '../../__mocks__/prisma'; +import { hook } from '../../__mocks__/utils/hook-wrapper'; +import { Result } from '../../utils/business'; import { checkClusterResources, checkProjectResources, @@ -20,9 +20,9 @@ import { deleteEnvironment, getProjectEnvironments, updateEnvironment, -} from './business.ts'; +} from './business'; -vi.mock('../../utils/hook-wrapper.ts', async () => ({ +vi.mock('../../utils/hook-wrapper', async () => ({ hook, })); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts index a28489487..4dc313c87 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts @@ -1,14 +1,14 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { addLogs, deleteEnvironment as deleteEnvironmentQuery, getEnvironmentsByProjectId, initializeEnvironment, updateEnvironment as updateEnvironmentQuery, -} from '@old-server/resources/queries-index.js'; -import type { Resources, UserDetails } from '@old-server/types/index.js'; -import { Result } from '@old-server/utils/business.js'; -import { hook } from '@old-server/utils/hook-wrapper.js'; +} from '@old-server/resources/queries-index'; +import type { Resources, UserDetails } from '@old-server/types/index'; +import { Result } from '@old-server/utils/business'; +import { hook } from '@old-server/utils/hook-wrapper'; import type { Cluster, Environment, diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts index cc6f8b9e1..78c0bc070 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Environment, Prisma, Project } from '@prisma/client'; // SELECT diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts index e8dfea278..c7f725405 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts @@ -6,18 +6,18 @@ import { import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; import { atDates, getProjectMockInfos, getUserMockInfos, -} from '../../utils/mocks.js'; -import * as business from './business.js'; +} from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessGetProjectEnvironmentsMock = vi.spyOn( diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts index 2cf9a9b11..4a462ea0b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts @@ -1,14 +1,14 @@ import { ProjectAuthorized, environmentContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401, -} from '@old-server/utils/errors.js'; +} from '@old-server/utils/errors'; import { checkEnvironmentCreate, @@ -17,7 +17,7 @@ import { deleteEnvironment, getProjectEnvironments, updateEnvironment, -} from './business.js'; +} from './business'; @Injectable() export class EnvironmentRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts index af71b1f20..3075b0dc7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts @@ -1,8 +1,8 @@ import { faker } from '@faker-js/faker'; import { describe, expect, it } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { getLogs } from './business.ts'; +import prisma from '../../__mocks__/prisma'; +import { getLogs } from './business'; describe('test log business', () => { it('should map filter (clean logs)', async () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts index cb25792c5..02f5d6941 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts @@ -1,6 +1,6 @@ import type { logContract } from '@cpn-console/shared'; import { CleanLogSchema } from '@cpn-console/shared'; -import { getAllLogs } from '@old-server/resources/queries-index.js'; +import { getAllLogs } from '@old-server/resources/queries-index'; export async function getLogs({ offset, diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts index 9985206f7..b4266368c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts @@ -1,5 +1,5 @@ import { exclude } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Log, Prisma, Project, User } from '@prisma/client'; // SELECT diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts index 358705b0a..e661aa013 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts @@ -2,14 +2,14 @@ import { logContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessGetLogsMock = vi.spyOn(business, 'getLogs'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts index 7de8f8bf0..9a21d8dec 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts @@ -1,15 +1,15 @@ import type { CleanLog, Log, XOR } from '@cpn-console/shared'; import { AdminAuthorized, logContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; +import { AppService } from '@old-server/app'; import type { UserProfile, UserProjectProfile, -} from '@old-server/utils/controller.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { Forbidden403 } from '@old-server/utils/errors.js'; +} from '@old-server/utils/controller'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403 } from '@old-server/utils/errors'; -import { getLogs } from './business.js'; +import { getLogs } from './business'; @Injectable() export class LogRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts index 1b7a6041d..18a4fb46b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts @@ -1,17 +1,17 @@ import type { XOR, projectMemberContract } from '@cpn-console/shared'; import { UserSchema } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { addLogs, deleteMember, listMembers as listMembersQuery, upsertMember, -} from '@old-server/resources/queries-index.js'; -import { BadRequest400, NotFound404 } from '@old-server/utils/errors.js'; -import { hook } from '@old-server/utils/hook-wrapper.js'; +} from '@old-server/resources/queries-index'; +import { BadRequest400, NotFound404 } from '@old-server/utils/errors'; +import { hook } from '@old-server/utils/hook-wrapper'; import type { Project, User } from '@prisma/client'; -import { logViaSession } from '../user/business.js'; +import { logViaSession } from '../user/business'; export const listMembers = async (projectId: Project['id']) => listMembersQuery(projectId); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts index 4400035bc..6cb1a10c6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Prisma, Project } from '@prisma/client'; export const listMembers = (projectId: Project['id']) => diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts index 5eb162fe7..9f35d4887 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts @@ -3,15 +3,15 @@ import { PROJECT_PERMS, projectMemberContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessListMembersMock = vi.spyOn(business, 'listMembers'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts index 0e9038fa6..4d086b1c0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts @@ -4,21 +4,21 @@ import { projectMemberContract, } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; import { ErrorResType, Forbidden403, NotFound404, Unauthorized401, -} from '@old-server/utils/errors.js'; +} from '@old-server/utils/errors'; import { addMember, listMembers, patchMembers, removeMember, -} from './business.js'; +} from './business'; @Injectable() export class ProjectMemberRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts index cbf7086c3..436b9180c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts @@ -2,15 +2,15 @@ import { faker } from '@faker-js/faker'; import type { ProjectMembers, ProjectRole, User } from '@prisma/client'; import { describe, expect, it } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { BadRequest400 } from '../../utils/errors.ts'; +import prisma from '../../__mocks__/prisma'; +import { BadRequest400 } from '../../utils/errors'; import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles, -} from './business.ts'; +} from './business'; const projectId = faker.string.uuid(); describe('test project-role business', () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts index 11aa5791b..f7e7efaf9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts @@ -1,12 +1,12 @@ import type { projectRoleContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { deleteRole as deleteRoleQuery, listMembers, listRoles as listRolesQuery, updateRole, -} from '@old-server/resources/queries-index.js'; -import { BadRequest400 } from '@old-server/utils/errors.js'; +} from '@old-server/resources/queries-index'; +import { BadRequest400 } from '@old-server/utils/errors'; import type { Project, ProjectRole } from '@prisma/client'; export async function listRoles(projectId: Project['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts index 1de745cd4..a3f575921 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Prisma, Project, ProjectRole } from '@prisma/client'; export const listRoles = (projectId: Project['id']) => diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts index 59a0bbf05..299e04ca0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts @@ -2,16 +2,16 @@ import { PROJECT_PERMS, projectRoleContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; -vi.mock('./business.js'); +vi.mock('./business'); vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts index aa872f2e6..230f7735e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts @@ -4,13 +4,13 @@ import { projectRoleContract, } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; import { ErrorResType, Forbidden403, NotFound404, -} from '@old-server/utils/errors.js'; +} from '@old-server/utils/errors'; import { countRolesMembers, @@ -18,7 +18,7 @@ import { deleteRole, listRoles, patchRoles, -} from './business.js'; +} from './business'; @Injectable() export class ProjectRoleRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts index 63074e103..6eeaa1354 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts @@ -15,7 +15,7 @@ import { getProjectStore, getPublicClusters, saveProjectStore, -} from '@old-server/resources/queries-index.js'; +} from '@old-server/resources/queries-index'; import type { Project, ProjectPlugin } from '@prisma/client'; export type ConfigRecords = { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts index 470bcc1db..0bceefba1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts @@ -1,7 +1,7 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Project } from '@prisma/client'; -import type { ConfigRecords } from './business.js'; +import type { ConfigRecords } from './business'; // CONFIG export function getProjectStore(projectId: Project['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts index 45e37dc77..eb2819915 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts @@ -2,14 +2,14 @@ import { PROJECT_PERMS, projectServiceContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessGetServicesMock = vi.spyOn(business, 'getProjectServices'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts index 445962628..285e204db 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts @@ -4,11 +4,11 @@ import { projectServiceContract, } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { Forbidden403, NotFound404 } from '@old-server/utils/errors.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403, NotFound404 } from '@old-server/utils/errors'; -import { getProjectServices, updateProjectServices } from './business.js'; +import { getProjectServices, updateProjectServices } from './business'; @Injectable() export class ProjectServiceRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts index 1ff117974..b9cd6c237 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts @@ -8,15 +8,15 @@ import type { } from '@prisma/client'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { hook } from '../../__mocks__/utils/hook-wrapper.ts'; +import prisma from '../../__mocks__/prisma'; +import { hook } from '../../__mocks__/utils/hook-wrapper'; import { BadRequest400, ErrorResType, Unprocessable422, -} from '../../utils/errors.js'; -import { dbToObj } from '../project-service/business.ts'; -import * as userBusiness from '../user/business.js'; +} from '../../utils/errors'; +import { dbToObj } from '../project-service/business'; +import * as userBusiness from '../user/business'; import { archiveProject, chunk, @@ -27,9 +27,9 @@ import { listProjects, replayHooks, updateProject, -} from './business.ts'; +} from './business'; -vi.mock('../../utils/hook-wrapper.ts', async () => ({ +vi.mock('../../utils/hook-wrapper', async () => ({ hook, })); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts index 46974dcd0..607f6f2fb 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts @@ -1,7 +1,7 @@ import { servicesInfos } from '@cpn-console/hooks'; import type { projectContract } from '@cpn-console/shared'; import { ProjectStatusSchema } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { addLogs, deleteAllEnvironmentForProject, @@ -13,17 +13,17 @@ import { listProjects as listProjectsQuery, lockProject, updateProject as updateProjectQuery, -} from '@old-server/resources/queries-index.js'; -import type { UserDetails } from '@old-server/types/index.js'; -import { whereBuilder } from '@old-server/utils/controller.js'; -import { parallelBulkLimit } from '@old-server/utils/env.js'; -import type { ErrorResType } from '@old-server/utils/errors.js'; +} from '@old-server/resources/queries-index'; +import type { UserDetails } from '@old-server/types/index'; +import { whereBuilder } from '@old-server/utils/controller'; +import { parallelBulkLimit } from '@old-server/utils/env'; +import type { ErrorResType } from '@old-server/utils/errors'; import { BadRequest400, Forbidden403, Unprocessable422, -} from '@old-server/utils/errors.js'; -import { hook } from '@old-server/utils/hook-wrapper.js'; +} from '@old-server/utils/errors'; +import { hook } from '@old-server/utils/hook-wrapper'; import type { Project, User } from '@prisma/client'; import { json2csv } from 'json-2-csv'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts index 419968b5a..e0f68e66c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts @@ -1,7 +1,7 @@ import type { XOR, projectContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; -import { appVersion } from '@old-server/utils/env.js'; -import { uuid } from '@old-server/utils/queries-tools.js'; +import prisma from '@old-server/prisma'; +import { appVersion } from '@old-server/utils/env'; +import { uuid } from '@old-server/utils/queries-tools'; import type { Prisma, Project, User } from '@prisma/client'; import { ProjectStatus } from '@prisma/client'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts index 05b7aa643..fd0be47b4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts @@ -3,20 +3,20 @@ import { PROJECT_PERMS, projectContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import type { UserDetails } from '../../types/index.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; +import app from '../../app'; +import type { UserDetails } from '../../types/index'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; import { getProjectMockInfos, getRandomRequestor, getUserMockInfos, -} from '../../utils/mocks.js'; -import * as business from './business.js'; +} from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessListMock = vi.spyOn(business, 'listProjects'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts index 80565b788..ffdbeacc5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts @@ -5,15 +5,15 @@ import { projectContract, } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401, -} from '@old-server/utils/errors.js'; +} from '@old-server/utils/errors'; import { archiveProject, @@ -25,7 +25,7 @@ import { listProjects, replayHooks, updateProject, -} from './business.js'; +} from './business'; @Injectable() export class ProjectRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts index 4d17aa435..d3dd04328 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts @@ -1,14 +1,14 @@ -export * from '@old-server/resources/admin-role/queries.js'; -export * from '@old-server/resources/cluster/queries.js'; -export * from '@old-server/resources/service-chain/queries.js'; -export * from '@old-server/resources/environment/queries.js'; -export * from '@old-server/resources/log/queries.js'; -export * from '@old-server/resources/project/queries.js'; -export * from '@old-server/resources/project-member/queries.js'; -export * from '@old-server/resources/project-role/queries.js'; -export * from '@old-server/resources/project-service/queries.js'; -export * from '@old-server/resources/repository/queries.js'; -export * from '@old-server/resources/user/queries.js'; -export * from '@old-server/resources/stage/queries.js'; -export * from '@old-server/resources/zone/queries.js'; -export * from '@old-server/resources/system/settings/queries.js'; +export * from '@old-server/resources/admin-role/queries'; +export * from '@old-server/resources/cluster/queries'; +export * from '@old-server/resources/service-chain/queries'; +export * from '@old-server/resources/environment/queries'; +export * from '@old-server/resources/log/queries'; +export * from '@old-server/resources/project/queries'; +export * from '@old-server/resources/project-member/queries'; +export * from '@old-server/resources/project-role/queries'; +export * from '@old-server/resources/project-service/queries'; +export * from '@old-server/resources/repository/queries'; +export * from '@old-server/resources/user/queries'; +export * from '@old-server/resources/stage/queries'; +export * from '@old-server/resources/zone/queries'; +export * from '@old-server/resources/system/settings/queries'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts index 1f20356c5..7c6433828 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts @@ -9,9 +9,9 @@ import { getProjectRepositories as getProjectRepositoriesQuery, initializeRepository, updateRepository as updateRepositoryQuery, -} from '@old-server/resources/queries-index.js'; -import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors.js'; -import { hook } from '@old-server/utils/hook-wrapper.js'; +} from '@old-server/resources/queries-index'; +import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors'; +import { hook } from '@old-server/utils/hook-wrapper'; import type { Project, Repository, User } from '@prisma/client'; export async function getProjectRepositories(projectId: Project['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts index 95bd00582..a7e54ee95 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Project, Repository } from '@prisma/client'; // SELECT diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts index 7e025ed47..eaae97830 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts @@ -2,19 +2,19 @@ import { PROJECT_PERMS, repositoryContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; import { atDates, getProjectMockInfos, getUserMockInfos, -} from '../../utils/mocks.js'; -import * as business from './business.js'; +} from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessCreateMock = vi.spyOn(business, 'createRepository'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts index 035ddc1a1..dabdfaf55 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts @@ -5,15 +5,15 @@ import { repositoryContract, } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; import { ErrorResType, Forbidden403, NotFound404, Unauthorized401, -} from '@old-server/utils/errors.js'; -import { filterObjectByKeys } from '@old-server/utils/queries-tools.js'; +} from '@old-server/utils/errors'; +import { filterObjectByKeys } from '@old-server/utils/queries-tools'; import { createRepository, @@ -21,7 +21,7 @@ import { getProjectRepositories, syncRepository, updateRepository, -} from './business.js'; +} from './business'; @Injectable() export class RepositoryRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts index 883e7e948..558fe4b1d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts @@ -21,7 +21,7 @@ import { listServiceChains, retryServiceChain, validateServiceChain, -} from './business.ts'; +} from './business'; vi.mock('axios'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts index bfe1ba32e..35aa603ef 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts @@ -4,7 +4,7 @@ import { listServiceChains as listServiceChainsQuery, retryServiceChain as retryServiceChainQuery, validateServiceChain as validateServiceChainQuery, -} from '@old-server/resources/queries-index.js'; +} from '@old-server/resources/queries-index'; export async function listServiceChains() { return listServiceChainsQuery(); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts index 1b7322956..61d60b78c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts @@ -17,14 +17,14 @@ import { import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessListServiceChainsMock = vi.spyOn(business, 'listServiceChains'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts index 0c8634cba..c64a4fc94 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts @@ -1,10 +1,10 @@ import type { AsyncReturnType } from '@cpn-console/shared'; import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import '@old-server/types/index.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { Forbidden403 } from '@old-server/utils/errors.js'; +import { AppService } from '@old-server/app'; +import '@old-server/types/index'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403 } from '@old-server/utils/errors'; import { getServiceChainDetails as getServiceChainDetailsBusiness, @@ -12,7 +12,7 @@ import { listServiceChains as listServiceChainsBusiness, retryServiceChain as retryServiceChainBusiness, validateServiceChain as validateServiceChainBusiness, -} from './business.js'; +} from './business'; @Injectable() export class ServiceChainRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts index e8b6cc45d..baf5ce6aa 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts @@ -2,16 +2,16 @@ import type { ServiceStatus } from '@cpn-console/hooks'; import { MonitorStatus, serviceContract } from '@cpn-console/shared'; import { describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; const authUserMock = vi.spyOn(utilsController, 'authUser'); vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const businessCheckMock = vi.spyOn(business, 'checkServicesHealth'); const businessRefreshMock = vi.spyOn(business, 'refreshServicesHealth'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts index cba22d5e8..f7f214716 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts @@ -1,10 +1,10 @@ import { AdminAuthorized, serviceContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { Forbidden403 } from '@old-server/utils/errors.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403 } from '@old-server/utils/errors'; -import { checkServicesHealth, refreshServicesHealth } from './business.js'; +import { checkServicesHealth, refreshServicesHealth } from './business'; @Injectable() export class ServiceMonitorRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts index e6dc2783f..235ba727c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts @@ -2,15 +2,15 @@ import { faker } from '@faker-js/faker'; import type { Environment, Stage } from '@prisma/client'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { BadRequest400, NotFound404 } from '../../utils/errors.ts'; +import prisma from '../../__mocks__/prisma'; +import { BadRequest400, NotFound404 } from '../../utils/errors'; import { createStage, deleteStage, getStageAssociatedEnvironments, listStages, updateStage, -} from './business.ts'; +} from './business'; describe('test stage busines logic', () => { let stage: Stage; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts index 6950805f3..ea3bbe6f2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts @@ -1,5 +1,5 @@ import type { CreateStageBody, UpdateStageBody } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { createStage as createStageQuery, deleteStage as deleteStageQuery, @@ -12,8 +12,8 @@ import { listStages as listStagesQuery, removeClusterFromStage, updateStageName, -} from '@old-server/resources/queries-index.js'; -import { BadRequest400, NotFound404 } from '@old-server/utils/errors.js'; +} from '@old-server/resources/queries-index'; +import { BadRequest400, NotFound404 } from '@old-server/utils/errors'; import type { Cluster, Stage } from '@prisma/client'; export async function getStageAssociatedEnvironments(stageId: Stage['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts index 874f84789..fb2af3b53 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Cluster, Stage } from '@prisma/client'; export function listStages() { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts index 62eed897a..f2531dc57 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts @@ -3,15 +3,15 @@ import { stageContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; -import { getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; +import { getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessListMock = vi.spyOn(business, 'listStages'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts index 4a6c7dc27..d49a04a52 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts @@ -1,8 +1,8 @@ import { AdminAuthorized, stageContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; import { createStage, @@ -10,7 +10,7 @@ import { getStageAssociatedEnvironments, listStages, updateStage, -} from './business.js'; +} from './business'; @Injectable() export class StageRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts index bcfeb5d9b..80776dc3a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import prisma from '../../../__mocks__/prisma.js'; -import { objToDb, updatePluginConfig } from './business.ts'; +import prisma from '../../../__mocks__/prisma'; +import { objToDb, updatePluginConfig } from './business'; describe('test system/config business', () => { const config = { test: { key1: 'value1' } }; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts index 07dd6acca..80c85851c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts @@ -4,9 +4,9 @@ import { servicesInfos, } from '@cpn-console/hooks'; import type { PluginsUpdateBody } from '@cpn-console/shared'; -import { BadRequest400 } from '@old-server/utils/errors.js'; +import { BadRequest400 } from '@old-server/utils/errors'; -import { getAdminPlugin, savePluginsConfig } from './queries.js'; +import { getAdminPlugin, savePluginsConfig } from './queries'; export type ConfigRecords = { key: string; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts index a19db0fce..c9e85b634 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts @@ -1,6 +1,6 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; -import type { ConfigRecords } from './business.js'; +import type { ConfigRecords } from './business'; // CONFIG export const getAdminPlugin = prisma.adminPlugin.findMany; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts index b428692b2..463a0b3f8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts @@ -1,15 +1,15 @@ import { systemPluginContract } from '@cpn-console/shared'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../../app.js'; -import * as utilsController from '../../../utils/controller.js'; -import { BadRequest400 } from '../../../utils/errors.js'; -import { getUserMockInfos } from '../../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../../app'; +import * as utilsController from '../../../utils/controller'; +import { BadRequest400 } from '../../../utils/errors'; +import { getUserMockInfos } from '../../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../../utils/mocks.js')).mockSessionPlugin, + (await import('../../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessGetPluginsConfigMock = vi.spyOn(business, 'getPluginsConfig'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts index 2237e5f82..6b5b8ec57 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts @@ -1,10 +1,10 @@ import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; -import { getPluginsConfig, updatePluginConfig } from './business.js'; +import { getPluginsConfig, updatePluginConfig } from './business'; @Injectable() export class SystemConfigRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts index 5ef6bc555..164ab508b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts @@ -1 +1 @@ -export * from './router.js'; +export * from './router'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts index 5d8a97777..0c58a3ae8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts @@ -1,11 +1,11 @@ import { systemContract } from '@cpn-console/shared'; import { describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; +import app from '../../app'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); describe('system - router', () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts index c53ef511f..17a2c4457 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts @@ -1,7 +1,7 @@ import { systemContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { appVersion } from '@old-server/utils/env.js'; +import { AppService } from '@old-server/app'; +import { appVersion } from '@old-server/utils/env'; @Injectable() export class SystemRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts index 1b3376299..80a8b0b8d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts @@ -3,7 +3,7 @@ import type { UpsertSystemSettingBody } from '@cpn-console/shared'; import { getSystemSettings as getSystemSettingsQuery, upsertSystemSetting as upsertSystemSettingQuery, -} from './queries.js'; +} from './queries'; export const getSystemSettings = (key?: string) => getSystemSettingsQuery({ key }); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts index 08e72e84c..252a58400 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Prisma, SystemSetting } from '@prisma/client'; export function upsertSystemSetting(newSystemSetting: SystemSetting) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts index 679c3b07a..6f135b3ae 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts @@ -1,14 +1,14 @@ import { systemSettingsContract } from '@cpn-console/shared'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../../app.js'; -import * as utilsController from '../../../utils/controller.js'; -import { getUserMockInfos } from '../../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../../app'; +import * as utilsController from '../../../utils/controller'; +import { getUserMockInfos } from '../../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../../utils/mocks.js')).mockSessionPlugin, + (await import('../../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessGetSystemSettingsMock = vi.spyOn(business, 'getSystemSettings'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts index fa43afb34..931e23fc6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts @@ -1,10 +1,10 @@ import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { Forbidden403 } from '@old-server/utils/errors.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403 } from '@old-server/utils/errors'; -import { getSystemSettings, upsertSystemSetting } from './business.js'; +import { getSystemSettings, upsertSystemSetting } from './business'; @Injectable() export class SystemSettingsRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts index 8f73f72da..ffb590492 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts @@ -1,8 +1,8 @@ import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import type { UserDetails } from '../../types/index.ts'; +import prisma from '../../__mocks__/prisma'; +import type { UserDetails } from '../../types/index'; import { TokenInvalidReason, getMatchingUsers, @@ -10,8 +10,8 @@ import { logViaSession, logViaToken, patchUsers, -} from './business.ts'; -import * as queries from './queries.js'; +} from './business'; +import * as queries from './queries'; const getUsersQueryMock = vi.spyOn(queries, 'getUsers'); const getMatchingUsersQueryMock = vi.spyOn(queries, 'getMatchingUsers'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts index 40fa43e9a..495b39011 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts @@ -1,11 +1,11 @@ import type { XOR, userContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { getMatchingUsers as getMatchingUsersQuery, getUsers as getUsersQuery, -} from '@old-server/resources/queries-index.js'; -import type { UserDetails } from '@old-server/types/index.js'; -import { BadRequest400 } from '@old-server/utils/errors.js'; +} from '@old-server/resources/queries-index'; +import type { UserDetails } from '@old-server/types/index'; +import { BadRequest400 } from '@old-server/utils/errors'; import type { AdminRole, AdminToken, diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts index 2b8eae2fb..0533a5001 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Prisma, User } from '@prisma/client'; type UserCreate = Omit; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts index 3c21c1073..9cd9ec655 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts @@ -2,14 +2,14 @@ import { userContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { getUserMockInfos, setRequestor } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { getUserMockInfos, setRequestor } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessGetMatchingMock = vi.spyOn(business, 'getMatchingUsers'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts index afea7a061..e664a22a7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts @@ -1,20 +1,20 @@ import { AdminAuthorized, userContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import '@old-server/types/index.js'; -import { authUser } from '@old-server/utils/controller.js'; +import { AppService } from '@old-server/app'; +import '@old-server/types/index'; +import { authUser } from '@old-server/utils/controller'; import { ErrorResType, Forbidden403, Unauthorized401, -} from '@old-server/utils/errors.js'; +} from '@old-server/utils/errors'; import { getMatchingUsers, getUsers, logViaSession, patchUsers, -} from './business.js'; +} from './business'; @Injectable() export class UserRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts index 7da8f6fa8..9ba5201f3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts @@ -1,10 +1,10 @@ import type { personalAccessTokenContract } from '@cpn-console/shared'; import { generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared'; -import { BadRequest400 } from '@old-server/utils/errors.js'; +import { BadRequest400 } from '@old-server/utils/errors'; import type { AdminToken, User } from '@prisma/client'; import { createHash } from 'node:crypto'; -import prisma from '../../../prisma.js'; +import prisma from '../../../prisma'; export async function listTokens(userId: User['id']) { return prisma.personalAccessToken.findMany({ diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts index e94defcf3..6606fd495 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts @@ -1,11 +1,11 @@ import { personalAccessTokenContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import '@old-server/types/index.js'; -import { authUser } from '@old-server/utils/controller.js'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors.js'; +import { AppService } from '@old-server/app'; +import '@old-server/types/index'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; -import { createToken, deleteToken, listTokens } from './business.js'; +import { createToken, deleteToken, listTokens } from './business'; @Injectable() export class UserTokensRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts index 2298b5de9..f51967e21 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts @@ -2,16 +2,16 @@ import { faker } from '@faker-js/faker'; import type { Cluster, Zone } from '@prisma/client'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import prisma from '../../__mocks__/prisma.js'; -import { hook } from '../../__mocks__/utils/hook-wrapper.ts'; -import { BadRequest400 } from '../../utils/errors.ts'; -import { createZone, deleteZone, listZones, updateZone } from './business.ts'; -import * as queries from './queries.js'; +import prisma from '../../__mocks__/prisma'; +import { hook } from '../../__mocks__/utils/hook-wrapper'; +import { BadRequest400 } from '../../utils/errors'; +import { createZone, deleteZone, listZones, updateZone } from './business'; +import * as queries from './queries'; const userId = faker.string.uuid(); const reqId = faker.string.uuid(); const linkZoneToClustersMock = vi.spyOn(queries, 'linkZoneToClusters'); -vi.mock('../../utils/hook-wrapper.ts', async () => ({ +vi.mock('../../utils/hook-wrapper', async () => ({ hook, })); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts index db98b62c0..9201d0554 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts @@ -1,10 +1,10 @@ import type { User, Zone } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; -import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors.js'; -import { hook } from '@old-server/utils/hook-wrapper.js'; +import prisma from '@old-server/prisma'; +import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors'; +import { hook } from '@old-server/utils/hook-wrapper'; -import { addLogs } from '../queries-index.js'; -import { linkZoneToClusters } from './queries.js'; +import { addLogs } from '../queries-index'; +import { linkZoneToClusters } from './queries'; export const listZones = prisma.zone.findMany; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts index 65ae14df1..6295fcdc0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts @@ -1,4 +1,4 @@ -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import type { Cluster, Zone } from '@prisma/client'; export function getZoneByIdOrThrow(id: Zone['id']) { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts index 2d992ee64..9218a0ba8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts @@ -3,15 +3,15 @@ import { zoneContract } from '@cpn-console/shared'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import app from '../../app.js'; -import * as utilsController from '../../utils/controller.js'; -import { BadRequest400 } from '../../utils/errors.js'; -import { getUserMockInfos } from '../../utils/mocks.js'; -import * as business from './business.js'; +import app from '../../app'; +import * as utilsController from '../../utils/controller'; +import { BadRequest400 } from '../../utils/errors'; +import { getUserMockInfos } from '../../utils/mocks'; +import * as business from './business'; vi.mock( 'fastify-keycloak-adapter', - (await import('../../utils/mocks.js')).mockSessionPlugin, + (await import('../../utils/mocks')).mockSessionPlugin, ); const authUserMock = vi.spyOn(utilsController, 'authUser'); const businessListMock = vi.spyOn(business, 'listZones'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts index d9c158e20..7da2f17b1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts @@ -1,14 +1,14 @@ import { AdminAuthorized, zoneContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app.js'; -import { authUser } from '@old-server/utils/controller.js'; +import { AppService } from '@old-server/app'; +import { authUser } from '@old-server/utils/controller'; import { ErrorResType, Forbidden403, Unauthorized401, -} from '@old-server/utils/errors.js'; +} from '@old-server/utils/errors'; -import { createZone, deleteZone, listZones, updateZone } from './business.js'; +import { createZone, deleteZone, listZones, updateZone } from './business'; @Injectable() export class ZoneRouterService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts index 0d3113084..b73fdf72f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts @@ -1,19 +1,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { logger } from './app.js'; -import { closeConnections } from './connect.js'; -import { exitGracefully, handleExit } from './server.js'; +import { logger } from './app'; +import { closeConnections } from './connect'; +import { exitGracefully, handleExit } from './server'; vi.mock( 'fastify-keycloak-adapter', - (await import('./utils/mocks.js')).mockSessionPlugin, + (await import('./utils/mocks')).mockSessionPlugin, ); -vi.mock('./init/db/index.js', () => ({ initDb: vi.fn() })); -vi.mock('./connect.js'); +vi.mock('./init/db/index', () => ({ initDb: vi.fn() })); +vi.mock('./connect'); process.exit = vi.fn(); -vi.mock('./prepare-app.js', () => { +vi.mock('./prepare-app', () => { const app = { listen: vi.fn(), close: vi.fn(async () => {}), diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts index 466b80b65..62b6ad380 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts @@ -14,7 +14,7 @@ import keycloak from 'fastify-keycloak-adapter'; import { AppService } from './app'; import { ConnectionService } from './connect'; import { PrepareAppService } from './prepare-app'; -import { ResourcesService } from './resources/index.js'; +import { ResourcesService } from './resources/index'; import { isCI, isDev, @@ -23,11 +23,11 @@ import { isProd, isTest, port, -} from './utils/env.js'; -import { FastifyService } from './utils/fastify.js'; -import { keycloakConf, sessionConf } from './utils/keycloak.js'; -import type { CustomLogger } from './utils/logger.js'; -import { LoggerService } from './utils/logger.js'; +} from './utils/env'; +import { FastifyService } from './utils/fastify'; +import { keycloakConf, sessionConf } from './utils/keycloak'; +import type { CustomLogger } from './utils/logger'; +import { LoggerService } from './utils/logger'; @Injectable() export class ServerService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts index e8f91a65b..4cc4d579c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts @@ -3,7 +3,7 @@ import { parseZodError, } from '@cpn-console/shared'; -import { BadRequest400 } from './errors.js'; +import { BadRequest400 } from './errors'; export type Success = Result; export type Failure = Result; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts index 438067a6e..46a00ced3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts @@ -5,12 +5,12 @@ import { projectIsLockedInfo, tokenHeaderName, } from '@cpn-console/shared'; -import prisma from '@old-server/prisma.js'; +import prisma from '@old-server/prisma'; import { logViaSession, logViaToken, -} from '@old-server/resources/user/business.js'; -import type { UserDetails } from '@old-server/types/index.js'; +} from '@old-server/resources/user/business'; +import type { UserDetails } from '@old-server/types/index'; import type { Cluster, Prisma, @@ -20,8 +20,8 @@ import type { } from '@prisma/client'; import type { FastifyRequest } from 'fastify'; -import { Unauthorized401 } from './errors.js'; -import { uuid } from './queries-tools.js'; +import { Unauthorized401 } from './errors'; +import { uuid } from './queries-tools'; export type RequireOnlyOne = Pick< T, diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts index 694a66517..4e85b86c4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { getJSDateFromUtcIso } from './date.js'; +import { getJSDateFromUtcIso } from './date'; describe('date-util', () => { it('should return a native Date object', () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts index 7fdca91b0..d10161411 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts @@ -12,8 +12,8 @@ import { keycloakClientSecret, keycloakRealm, keycloakRedirectUri, -} from './env.js'; -import { LoggerService } from './logger.js'; +} from './env'; +import { LoggerService } from './logger'; @Injectable() export class FastifyService { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts index c198f5176..7d72a70fe 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts @@ -6,8 +6,8 @@ import type { } from '@cpn-console/hooks'; import { describe, expect, it } from 'vitest'; -import type { ProjectInfos, ReposCreds } from './hook-wrapper.ts'; -import { transformToHookProject } from './hook-wrapper.ts'; +import type { ProjectInfos, ReposCreds } from './hook-wrapper'; +import { transformToHookProject } from './hook-wrapper'; const associatedCluster = { id: 'f0e39981-0b6d-4c16-aa96-225062b75767', diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts index 8e0373145..f0d639def 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts @@ -16,8 +16,8 @@ import { getPermsByUserRoles, resourceListToDict, } from '@cpn-console/shared'; -import type { ConfigRecords } from '@old-server/resources/project-service/business.js'; -import { dbToObj } from '@old-server/resources/project-service/business.js'; +import type { ConfigRecords } from '@old-server/resources/project-service/business'; +import { dbToObj } from '@old-server/resources/project-service/business'; import { archiveProject, getAdminPlugin, @@ -33,7 +33,7 @@ import { updateProjectCreated, updateProjectFailed, updateProjectWarning, -} from '@old-server/resources/queries-index.js'; +} from '@old-server/resources/queries-index'; import type { Cluster, Kubeconfig, @@ -42,7 +42,7 @@ import type { Zone, } from '@prisma/client'; -import { genericProxy } from './proxy.js'; +import { genericProxy } from './proxy'; export type ReposCreds = Record; export type ProjectInfos = AsyncReturnType; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts index ccdf1a23b..1675bb6be 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { userPayloadMapper } from './keycloak-utils.js'; +import { userPayloadMapper } from './keycloak-utils'; describe('keycloak', () => { it('should map keycloak user object to DSO user object without groups', () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts index a0da3e46b..429b3f8bb 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts @@ -13,8 +13,8 @@ import { keycloakRealm, keycloakRedirectUri, sessionSecret, -} from './env.js'; -import { bypassFn, userPayloadMapper } from './keycloak-utils.js'; +} from './env'; +import { bypassFn, userPayloadMapper } from './keycloak-utils'; export const keycloakConf = { appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts index 93c86f7bb..c784590ee 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts @@ -9,8 +9,8 @@ import { faker } from '@faker-js/faker'; import type { Repository } from '@prisma/client'; import fp from 'fastify-plugin'; -import type { UserDetails } from '../types/index.js'; -import type * as utilsController from '../utils/controller.js'; +import type { UserDetails } from '../types/index'; +import type * as utilsController from '../utils/controller'; let requestor: Requestor; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts index 79fc98b7f..843158f6d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts @@ -1,6 +1,6 @@ import type { PluginManagerOptions } from '@cpn-console/hooks'; -import { isCI, isInt, isProd } from './env.js'; +import { isCI, isInt, isProd } from './env'; export const pluginManagerOptions: PluginManagerOptions = { mockHooks: isCI || (!isProd && !isInt), diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts index 30a351735..9ae100961 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { genericProxy } from './proxy.js'; +import { genericProxy } from './proxy'; // Création d'une cible de test const target = { diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts index 9dffdd343..c92489a9a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts @@ -1,7 +1,7 @@ import { exclude } from '@cpn-console/shared'; import { describe, expect, it } from 'vitest'; -import { filterObjectByKeys } from './queries-tools.js'; +import { filterObjectByKeys } from './queries-tools'; describe('queries-tools', () => { it('should return a filtered object (filterObjectByKeys)', () => { diff --git a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts index d30d9a227..68cb46a9a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts @@ -10,6 +10,6 @@ export default defineConfig({ }, }, test: { - poolMatchGlobs: [['**/resources/**/*.spec.ts', 'forks']], + poolMatchGlobs: [['**/resources/**/*.spec', 'forks']], }, } as any); diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts index 93ffd45d4..8d234533c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts @@ -17,16 +17,16 @@ export default mergeConfig( exclude: [ '**/types', '**/mocks', - '**/*.spec.ts', - '**/*.d.ts', + '**/*.spec', + '**/*.d', '**/*.vue', - '**/queries.ts', - '**/mocks.ts', + '**/queries', + '**/mocks', ], }, include: ['src/**/*.spec.{ts,js}'], exclude: [...configDefaults.exclude, 'e2e/*'], - setupFiles: ['./vitest-init.ts'], + setupFiles: ['./vitest-init'], root: __dirname, pool: 'forks', }, From 22e777452aee23c454f399482b3589c075c08229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 9 Dec 2025 16:49:10 +0100 Subject: [PATCH 09/33] chore(gitlab-plugin): interalize awaited import to avoid top-level-await errors --- plugins/gitlab/src/project.ts | 47 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/plugins/gitlab/src/project.ts b/plugins/gitlab/src/project.ts index de0eb45c2..df3dc8e67 100644 --- a/plugins/gitlab/src/project.ts +++ b/plugins/gitlab/src/project.ts @@ -1,30 +1,37 @@ import * as fs from 'node:fs/promises' import path from 'node:path' -import { getApi } from './utils.js' -const baseDir = path.resolve(import.meta.url, '../../files/').split(':')[1] +import { getApi } from './utils.js' -const gitlabCiYml = (await fs.readFile(`${baseDir}/.gitlab-ci.yml`)).toString() -const mirrorSh = (await fs.readFile(`${baseDir}/mirror.sh`)).toString() +export async function provisionMirror(repoId: number) { + const baseDir = path.resolve(import.meta.url, '../../files/').split(':')[1] -const mirrorFirstActions: CommitAction[] = [ - { - action: 'create', - filePath: '.gitlab-ci.yml', - content: gitlabCiYml, - execute_filemode: false, - }, - { - action: 'create', - filePath: 'mirror.sh', - content: mirrorSh, - execute_filemode: true, - }, -] + const gitlabCiYml = ( + await fs.readFile(`${baseDir}/.gitlab-ci.yml`) + ).toString() + const mirrorSh = (await fs.readFile(`${baseDir}/mirror.sh`)).toString() -export async function provisionMirror(repoId: number) { + const mirrorFirstActions: CommitAction[] = [ + { + action: 'create', + filePath: '.gitlab-ci.yml', + content: gitlabCiYml, + execute_filemode: false, + }, + { + action: 'create', + filePath: 'mirror.sh', + content: mirrorSh, + execute_filemode: true, + }, + ] const api = getApi() - await api.Commits.create(repoId, 'main', 'ci: :construction_worker: first mirror', mirrorFirstActions) + await api.Commits.create( + repoId, + 'main', + 'ci: :construction_worker: first mirror', + mirrorFirstActions, + ) } interface CommitAction { From f2eefc620a6eadf07113f0811a17e2a897636a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 9 Dec 2025 16:50:07 +0100 Subject: [PATCH 10/33] chore: add old-server new Nest.js services to CpinModule --- .../server-nestjs/src/cpin-module/cpin.module.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/server-nestjs/src/cpin-module/cpin.module.ts b/apps/server-nestjs/src/cpin-module/cpin.module.ts index 4c39d4d2a..3624bfc95 100644 --- a/apps/server-nestjs/src/cpin-module/cpin.module.ts +++ b/apps/server-nestjs/src/cpin-module/cpin.module.ts @@ -1,8 +1,22 @@ import { Module } from '@nestjs/common'; +import { AppService } from '@old-server/app'; +import { ConnectionService } from '@old-server/connect'; +import { PrepareAppService } from '@old-server/prepare-app'; +import { ResourcesService } from '@old-server/resources'; import { ServerService } from '@old-server/server'; +import { FastifyService } from '@old-server/utils/fastify'; +import { LoggerService } from '@old-server/utils/logger'; @Module({ controllers: [], - providers: [ServerService], + providers: [ + AppService, + ConnectionService, + FastifyService, + LoggerService, + PrepareAppService, + ResourcesService, + ServerService, + ], }) export class CpinModule {} From 378ea61cb94e26e21d8f6a394891cd99a4cd7ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 9 Dec 2025 16:50:40 +0100 Subject: [PATCH 11/33] chore: fix any type in AppService --- apps/server-nestjs/src/cpin-module/old-server/src/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts index 1530ab448..04942dee2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts @@ -7,7 +7,7 @@ import fastifySwaggerUi from '@fastify/swagger-ui'; import { Injectable } from '@nestjs/common'; import { initServer } from '@ts-rest/fastify'; import { generateOpenApi } from '@ts-rest/open-api'; -import type { FastifyRequest } from 'fastify'; +import type { FastifyInstance, FastifyRequest } from 'fastify'; import fastify from 'fastify'; import keycloak from 'fastify-keycloak-adapter'; @@ -28,7 +28,7 @@ export class AppService { serverInstance: ReturnType = initServer(); - app: any; + app: FastifyInstance; logger: any; async init() { From 0dc0020a2825eb1957961089f6a65833703bdfc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 9 Dec 2025 17:12:55 +0100 Subject: [PATCH 12/33] chore: disable tests from nest.js for now --- apps/server-nestjs/{test => test.backup}/app.e2e-spec.ts | 4 ++-- apps/server-nestjs/{test => test.backup}/jest-e2e.json | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename apps/server-nestjs/{test => test.backup}/app.e2e-spec.ts (88%) rename apps/server-nestjs/{test => test.backup}/jest-e2e.json (100%) diff --git a/apps/server-nestjs/test/app.e2e-spec.ts b/apps/server-nestjs/test.backup/app.e2e-spec.ts similarity index 88% rename from apps/server-nestjs/test/app.e2e-spec.ts rename to apps/server-nestjs/test.backup/app.e2e-spec.ts index cb271d715..4ec856c86 100644 --- a/apps/server-nestjs/test/app.e2e-spec.ts +++ b/apps/server-nestjs/test.backup/app.e2e-spec.ts @@ -3,14 +3,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; import { App } from 'supertest/types'; -import { AppModule } from './../src/app.module'; +import { MainModule } from './../src/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], + imports: [MainModule], }).compile(); app = moduleFixture.createNestApplication(); diff --git a/apps/server-nestjs/test/jest-e2e.json b/apps/server-nestjs/test.backup/jest-e2e.json similarity index 100% rename from apps/server-nestjs/test/jest-e2e.json rename to apps/server-nestjs/test.backup/jest-e2e.json From 2c44fceac0878a25377b22d7a2e1e87a5fc4277b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 9 Dec 2025 17:13:41 +0100 Subject: [PATCH 13/33] chore: rename nest.js base AppModule to MainModule for clarification with our own AppService --- apps/server-nestjs/src/{app.module.ts => main.module.ts} | 2 +- apps/server-nestjs/src/main.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename apps/server-nestjs/src/{app.module.ts => main.module.ts} (91%) diff --git a/apps/server-nestjs/src/app.module.ts b/apps/server-nestjs/src/main.module.ts similarity index 91% rename from apps/server-nestjs/src/app.module.ts rename to apps/server-nestjs/src/main.module.ts index 75567a420..9f6f3ad8f 100644 --- a/apps/server-nestjs/src/app.module.ts +++ b/apps/server-nestjs/src/main.module.ts @@ -9,4 +9,4 @@ import { CpinModule } from './cpin-module/cpin.module'; controllers: [], providers: [], }) -export class AppModule {} +export class MainModule {} diff --git a/apps/server-nestjs/src/main.ts b/apps/server-nestjs/src/main.ts index 23fc8c0fd..e5c6e0f38 100644 --- a/apps/server-nestjs/src/main.ts +++ b/apps/server-nestjs/src/main.ts @@ -1,9 +1,9 @@ import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; +import { MainModule } from './main.module'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(MainModule); await app.listen(process.env.PORT ?? 8080); } bootstrap(); From 667a69ce522a61d6ceb7cb314791943a4a654cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Wed, 10 Dec 2025 17:01:09 +0100 Subject: [PATCH 14/33] chore: add server-nestjs README to explain what we are doing --- apps/server-nestjs/README.md | 321 ++++++++++++++++++++++++----------- 1 file changed, 226 insertions(+), 95 deletions(-) diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md index d30c94649..192c496af 100644 --- a/apps/server-nestjs/README.md +++ b/apps/server-nestjs/README.md @@ -1,98 +1,229 @@ -

- Nest Logo -

- -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Discord -Backers on Open Collective -Sponsors on Open Collective - Donate us - Support us - Follow us on Twitter -

- - -## Description - -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - -## Project setup - -```bash -$ pnpm install +# À propos + +Ce dossier contient une nouvelle version de `server`, basée sur NestJS. + +On va profiter de cette nouvelle mouture pour passer de ça : + +```mermaid +flowchart TD + + %% --- Top-level --- + NestJS["Nest.js"] + FutursModules["Futurs modules
Nest.js correctement
découpés..."] + MainModule["MainModule"] + + NestJS --> MainModule + MainModule --> CPinModule + MainModule -.-> FutursModules + + CPinModule["CPinModule
(Contient TOUT le code actuel de 'server')"] + + %% --- Core services --- + ConnectionService["ConnectionService"] + LoggerService["LoggerService"] + FastifyService["FastifyService"] + ServerService["ServerService"] + AppService["AppService"] + ResourceService["ResourceService"] + PrepareAppService["PrepareAppService"] + InitDbService["InitDbService"] + PluginService["PluginService"] + + %% --- Router services --- + AdminRoleRouterService["AdminRoleRouterService"] + AdminTokenRouterService["AdminTokenRouterService"] + ClusterRouterService["ClusterRouterService"] + OtherRouterService["...RouterService"] + + %% --- External services --- + Gitlab["Gitlab"] + ArgoCD["ArgoCD"] + Dots["..."] + + %% --- CPinModule connections --- + CPinModule --> ConnectionService + CPinModule --> AppService + CPinModule --> LoggerService + CPinModule --> FastifyService + CPinModule --> ServerService + CPinModule --> ResourceService + CPinModule --> PrepareAppService + + %% --- AppService central connections --- + ConnectionService --> AppService + LoggerService --> AppService + FastifyService --> AppService + ServerService --> AppService + + AppService --> ResourceService + AppService --> PrepareAppService + AppService --> ServerService + AppService --> FastifyService + AppService --> LoggerService + AppService --> ConnectionService + + %% --- ResourceService to routers --- + ResourceService --> AdminRoleRouterService + ResourceService --> AdminTokenRouterService + ResourceService --> ClusterRouterService + ResourceService --> OtherRouterService + + %% --- PrepareAppService --- + PrepareAppService --> InitDbService + PrepareAppService --> PluginService + PrepareAppService --> ServerService + + %% --- PluginService external interactions --- + PluginService --> Gitlab + PluginService --> ArgoCD + PluginService --> Dots + + %% --- Bounding box (visual grouping only) --- + subgraph CPinBlock[" "] + CPinModule + ConnectionService + LoggerService + FastifyService + ServerService + AppService + ResourceService + PrepareAppService + InitDbService + PluginService + AdminRoleRouterService + AdminTokenRouterService + ClusterRouterService + OtherRouterService + end ``` -## Compile and run the project - -```bash -# development -$ pnpm run start - -# watch mode -$ pnpm run start:dev - -# production mode -$ pnpm run start:prod -``` - -## Run tests - -```bash -# unit tests -$ pnpm run test - -# e2e tests -$ pnpm run test:e2e - -# test coverage -$ pnpm run test:cov +à ça : + +```mermaid +flowchart TD + + %% --- Top-level Nest module --- + NestJS["Point d'entrée de NestJS"] + MainModule["MainModule"] + CPinModule["CPinModule"] + FutursModules["Futurs modules
NestJS correctement
découpés..."] + + NestJS --> MainModule + MainModule --> CPinModule + MainModule -.-> FutursModules + + %% --- Layering for clarity --- + subgraph LayerInit["Initialisation de l'application"] + InitAppService["InitAppService"] + InitDbService["InitDbService"] + PluginService["PluginService"] + end + + subgraph LayerCore["Coeur de l'application"] + AppService["AppService"] + RouterService["RouterService"] + ServerService["ServerService"] + end + + subgraph LayerInfra["Couche Infrastructure"] + LoggerService["LoggerService"] + ConfigurationService["ConfigurationService"] + DatabaseService["DatabaseService"] + FastifyService["FastifyService"] + AxiosService["AxiosService"] + end + + OtherAPIService["APIs externes
(par ex. OpenCDS)"] + AxiosService --> OtherAPIService + + subgraph LayerBusiness["Modules métiers"] + subgraph AdminRole["Admin Roles"] + AdminRoleRouterService["AdminRoleRouterService"] + AdminRoleBusinessService["AdminRoleBusinessService"] + AdminRoleDTOService["AdminRoleDTOService"] + AdminRoleRouterService --> AdminRoleBusinessService + AdminRoleRouterService --> LoggerService + AdminRoleBusinessService --> AdminRoleDTOService + AdminRoleBusinessService --> LoggerService + AdminRoleDTOService --> LoggerService + AdminRoleDTOService --> DatabaseService + end + subgraph AdminToken["Admin Tokens"] + AdminTokenRouterService["AdminTokenRouterService"] + AdminTokenBusinessService["AdminTokenBusinessService"] + AdminTokenDTOService["AdminTokenDTOService"] + AdminTokenRouterService --> AdminTokenBusinessService + AdminTokenRouterService --> LoggerService + AdminTokenBusinessService --> AdminTokenDTOService + AdminTokenBusinessService --> LoggerService + AdminTokenDTOService --> DatabaseService + AdminTokenDTOService --> LoggerService + end + subgraph ServiceChain["Service chains"] + ServiceChainRouterService["ServiceChainRouterService"] + ServiceChainBusinessService["ServiceChainBusinessService"] + ServiceChainRouterService --> ServiceChainBusinessService + ServiceChainRouterService --> LoggerService + ServiceChainBusinessService --> AxiosService + ServiceChainBusinessService --> LoggerService + + end + subgraph Cluster["Clusters"] + ClusterRouterService["ClusterRouterService"] + ClusterBusinessService["ClusterBusinessService"] + ClusterDTOService["ClusterDTOService"] + ClusterRouterService --> ClusterBusinessService + ClusterRouterService --> LoggerService + ClusterBusinessService --> ClusterDTOService + ClusterBusinessService --> LoggerService + ClusterDTOService --> DatabaseService + ClusterDTOService --> LoggerService + end + OtherBusinessModules["...Other Business Modules"] + end + + RouterService --> AdminRoleRouterService + RouterService --> AdminTokenRouterService + RouterService --> ClusterRouterService + RouterService --> ServiceChainRouterService + RouterService --> OtherBusinessModules + RouterService --> LoggerService + + subgraph LayerPlugins["Plugins compatibles CPiN"] + Gitlab["Gitlab"] + ArgoCD["ArgoCD"] + Kubernetes["Kubernetes"] + Dots["..."] + end + + %% --- Module wiring --- + CPinModule --> InitAppService + + %% Application initialization + InitAppService --> LoggerService + InitAppService --> ConfigurationService + InitAppService --> FastifyService + InitAppService --> AppService + InitAppService --> InitDbService + InitDbService --> DatabaseService + InitDbService --> LoggerService + InitAppService --> PluginService + InitAppService --> LoggerService + + %% App Core internal flow + AppService --> RouterService + AppService --> ServerService + AppService --> LoggerService + ServerService --> LoggerService + + %% Plugin Management + PluginService --> Gitlab + PluginService --> ArgoCD + PluginService --> Kubernetes + PluginService --> Dots + PluginService --> LoggerService + Gitlab --> LoggerService + ArgoCD --> LoggerService + Kubernetes --> LoggerService + Dots --> LoggerService ``` - -## Deployment - -When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. - -If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: - -```bash -$ pnpm install -g @nestjs/mau -$ mau deploy -``` - -With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. - -## Resources - -Check out a few resources that may come in handy when working with NestJS: - -- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. -- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). -- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). -- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. -- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). -- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). -- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). -- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). - -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) - -## License - -Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). From a3c0c9227ba5093955b25fbc8c62e7351ba9ed12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Wed, 10 Dec 2025 17:42:49 +0100 Subject: [PATCH 15/33] chore(server-nestjs): remove test scripts (we'll migrate to vitest soon) --- apps/server-nestjs/package.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/server-nestjs/package.json b/apps/server-nestjs/package.json index 37c00e837..de827ab2b 100644 --- a/apps/server-nestjs/package.json +++ b/apps/server-nestjs/package.json @@ -12,12 +12,7 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" }, "dependencies": { "@cpn-console/argocd-plugin": "workspace:^", From 29257ea20da333342ad25b5a9e3aa39372c237af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Fri, 12 Dec 2025 15:06:01 +0100 Subject: [PATCH 16/33] chore(nest-js): initialize a bunch of module/services to move old-server code --- apps/server-nestjs/README.md | 42 +++++++++---------- ...application-initialization.service.spec.ts | 18 ++++++++ .../application-initialization.service.ts | 4 ++ .../application-initialization.module.ts | 15 +++++++ .../database-initialization.service.spec.ts | 18 ++++++++ .../database-initialization.service.ts | 4 ++ .../plugin-management.service.spec.ts | 18 ++++++++ .../plugin-management.service.ts | 4 ++ .../src/cpin-module/cpin.module.ts | 10 ++++- .../configuration.service.spec.ts | 18 ++++++++ .../configuration/configuration.service.ts | 4 ++ .../database/database.service.spec.ts | 18 ++++++++ .../database/database.service.ts | 4 ++ .../fastify/fastify.service.spec.ts | 18 ++++++++ .../infrastructure/fastify/fastify.service.ts | 4 ++ .../http-client/http-client.service.spec.ts | 18 ++++++++ .../http-client/http-client.service.ts | 4 ++ .../infrastructure/infrastructure.module.ts | 11 +++++ .../logger/logger.service.spec.ts | 18 ++++++++ .../infrastructure/logger/logger.service.ts | 4 ++ 20 files changed, 231 insertions(+), 23 deletions(-) create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.ts diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md index 192c496af..6d1a8b473 100644 --- a/apps/server-nestjs/README.md +++ b/apps/server-nestjs/README.md @@ -114,9 +114,9 @@ flowchart TD %% --- Layering for clarity --- subgraph LayerInit["Initialisation de l'application"] - InitAppService["InitAppService"] - InitDbService["InitDbService"] - PluginService["PluginService"] + ApplicationInitializationService["ApplicationInitializationService"] + DatabaseInitializationService["DatabaseInitializationService"] + PluginManagementService["PluginManagementService"] end subgraph LayerCore["Coeur de l'application"] @@ -130,11 +130,11 @@ flowchart TD ConfigurationService["ConfigurationService"] DatabaseService["DatabaseService"] FastifyService["FastifyService"] - AxiosService["AxiosService"] + HTTPClientService["HTTPClientService"] end OtherAPIService["APIs externes
(par ex. OpenCDS)"] - AxiosService --> OtherAPIService + HTTPClientService --> OtherAPIService subgraph LayerBusiness["Modules métiers"] subgraph AdminRole["Admin Roles"] @@ -164,7 +164,7 @@ flowchart TD ServiceChainBusinessService["ServiceChainBusinessService"] ServiceChainRouterService --> ServiceChainBusinessService ServiceChainRouterService --> LoggerService - ServiceChainBusinessService --> AxiosService + ServiceChainBusinessService --> HTTPClientService ServiceChainBusinessService --> LoggerService end @@ -197,18 +197,18 @@ flowchart TD end %% --- Module wiring --- - CPinModule --> InitAppService + CPinModule --> ApplicationInitializationService %% Application initialization - InitAppService --> LoggerService - InitAppService --> ConfigurationService - InitAppService --> FastifyService - InitAppService --> AppService - InitAppService --> InitDbService - InitDbService --> DatabaseService - InitDbService --> LoggerService - InitAppService --> PluginService - InitAppService --> LoggerService + ApplicationInitializationService --> LoggerService + ApplicationInitializationService --> ConfigurationService + ApplicationInitializationService --> FastifyService + ApplicationInitializationService --> AppService + ApplicationInitializationService --> DatabaseInitializationService + DatabaseInitializationService --> DatabaseService + DatabaseInitializationService --> LoggerService + ApplicationInitializationService --> PluginManagementService + ApplicationInitializationService --> LoggerService %% App Core internal flow AppService --> RouterService @@ -217,11 +217,11 @@ flowchart TD ServerService --> LoggerService %% Plugin Management - PluginService --> Gitlab - PluginService --> ArgoCD - PluginService --> Kubernetes - PluginService --> Dots - PluginService --> LoggerService + PluginManagementService --> Gitlab + PluginManagementService --> ArgoCD + PluginManagementService --> Kubernetes + PluginManagementService --> Dots + PluginManagementService --> LoggerService Gitlab --> LoggerService ArgoCD --> LoggerService Kubernetes --> LoggerService diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts new file mode 100644 index 000000000..aa3e1a714 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationInitializationService } from './application-initialization.service'; + +describe('ApplicationInitializationServiceService', () => { + let service: ApplicationInitializationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ApplicationInitializationService], + }).compile(); + + service = module.get(ApplicationInitializationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts new file mode 100644 index 000000000..308131740 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ApplicationInitializationService {} diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts new file mode 100644 index 000000000..b4dc622c7 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; + +import { ApplicationInitializationService } from './application-initialization-service/application-initialization.service'; +import { DatabaseInitializationService } from './database-initialization/database-initialization.service'; +import { PluginManagementService } from './plugin-management/plugin-management.service'; + +@Module({ + providers: [ + ApplicationInitializationService, + DatabaseInitializationService, + PluginManagementService, + ], + exports: [ApplicationInitializationService], +}) +export class ApplicationInitializationModule {} diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts new file mode 100644 index 000000000..2be00355c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DatabaseInitializationService } from './database-initialization.service'; + +describe('DatabaseInitializationService', () => { + let service: DatabaseInitializationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DatabaseInitializationService], + }).compile(); + + service = module.get(DatabaseInitializationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts new file mode 100644 index 000000000..825e028ab --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class DatabaseInitializationService {} diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.spec.ts b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.spec.ts new file mode 100644 index 000000000..a29b76f88 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PluginManagementService } from './plugin-management.service'; + +describe('PluginManagementService', () => { + let service: PluginManagementService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PluginManagementService], + }).compile(); + + service = module.get(PluginManagementService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts new file mode 100644 index 000000000..02722c011 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class PluginManagementService {} diff --git a/apps/server-nestjs/src/cpin-module/cpin.module.ts b/apps/server-nestjs/src/cpin-module/cpin.module.ts index 3624bfc95..15a7406c1 100644 --- a/apps/server-nestjs/src/cpin-module/cpin.module.ts +++ b/apps/server-nestjs/src/cpin-module/cpin.module.ts @@ -5,18 +5,24 @@ import { PrepareAppService } from '@old-server/prepare-app'; import { ResourcesService } from '@old-server/resources'; import { ServerService } from '@old-server/server'; import { FastifyService } from '@old-server/utils/fastify'; -import { LoggerService } from '@old-server/utils/logger'; +import { CustomLoggerService } from '@old-server/utils/logger'; +import { ApplicationInitializationModule } from './application-initialization/application-initialization.module'; +import { InfrastructureModule } from './infrastructure/infrastructure.module'; +// This module host the old "server code" of our backend. +// It it means to be empty in the future, by extracting from it +// as many modules as possible ! @Module({ controllers: [], providers: [ AppService, ConnectionService, FastifyService, - LoggerService, + CustomLoggerService, PrepareAppService, ResourcesService, ServerService, ], + imports: [ApplicationInitializationModule, InfrastructureModule], }) export class CpinModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.spec.ts new file mode 100644 index 000000000..a49109e4c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigurationService } from './configuration.service'; + +describe('ConfigurationService', () => { + let service: ConfigurationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ConfigurationService], + }).compile(); + + service = module.get(ConfigurationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts new file mode 100644 index 000000000..f2d3dae67 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ConfigurationService {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.spec.ts new file mode 100644 index 000000000..b806f3163 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DatabaseService } from './database.service'; + +describe('DatabaseService', () => { + let service: DatabaseService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DatabaseService], + }).compile(); + + service = module.get(DatabaseService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts new file mode 100644 index 000000000..f0ff1df3a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class DatabaseService {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.spec.ts new file mode 100644 index 000000000..6c473f5b1 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FastifyService } from './fastify.service'; + +describe('FastifyService', () => { + let service: FastifyService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [FastifyService], + }).compile(); + + service = module.get(FastifyService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.ts new file mode 100644 index 000000000..7f662a41f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FastifyService {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.spec.ts new file mode 100644 index 000000000..0e6d6797b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpClientService } from './http-client.service'; + +describe('HttpClientService', () => { + let service: HttpClientService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [HttpClientService], + }).compile(); + + service = module.get(HttpClientService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.ts new file mode 100644 index 000000000..78dfa1769 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class HttpClientService {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts new file mode 100644 index 000000000..7657aa206 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { LoggerService } from './logger/logger.service'; +import { DatabaseService } from './database/database.service'; +import { HttpClientService } from './http-client/http-client.service'; +import { FastifyService } from './fastify/fastify.service'; +import { ConfigurationService } from './configuration/configuration.service'; + +@Module({ + providers: [LoggerService, DatabaseService, HttpClientService, FastifyService, ConfigurationService] +}) +export class InfrastructureModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.spec.ts new file mode 100644 index 000000000..03ac92434 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LoggerService } from './logger.service'; + +describe('LoggerService', () => { + let service: LoggerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LoggerService], + }).compile(); + + service = module.get(LoggerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.ts new file mode 100644 index 000000000..5c31077ab --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LoggerService {} From c44e601e669a94e8bae3a3af6dbbb2a8a8d356c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Fri, 12 Dec 2025 15:08:41 +0100 Subject: [PATCH 17/33] chore(nest-js): update old-server code from main --- .../cpin-module/old-server/prisma.config.ts | 14 +- .../old-server/src/__mocks__/prisma.ts | 18 +- .../src/__mocks__/utils/hook-wrapper.ts | 56 +- .../cpin-module/old-server/src/app.spec.ts | 37 +- .../src/cpin-module/old-server/src/app.ts | 139 +-- .../old-server/src/connect.spec.ts | 132 +- .../src/cpin-module/old-server/src/connect.ts | 107 +- .../old-server/src/init/db/dump.ts | 36 +- .../old-server/src/init/db/index.ts | 101 +- .../old-server/src/init/db/utils.spec.ts | 83 +- .../old-server/src/init/db/utils.ts | 146 +-- .../old-server/src/mocks/prisma.ts | 18 +- .../cpin-module/old-server/src/mocks/utils.ts | 26 +- .../src/cpin-module/old-server/src/plugins.ts | 98 +- .../old-server/src/prepare-app.spec.ts | 115 +- .../cpin-module/old-server/src/prepare-app.ts | 206 ++-- .../src/cpin-module/old-server/src/prisma.ts | 6 +- .../migration.sql | 4 + .../src/prisma/schema/project.prisma | 3 + .../src/resources/admin-role/business.spec.ts | 398 +++--- .../src/resources/admin-role/business.ts | 173 ++- .../src/resources/admin-role/queries.ts | 54 +- .../src/resources/admin-role/router.spec.ts | 400 +++--- .../src/resources/admin-role/router.ts | 155 ++- .../resources/admin-token/business.spec.ts | 139 +-- .../src/resources/admin-token/business.ts | 140 +-- .../src/resources/admin-token/router.spec.ts | 343 +++--- .../src/resources/admin-token/router.ts | 96 +- .../src/resources/cluster/business.spec.ts | 452 +++---- .../src/resources/cluster/business.ts | 455 +++---- .../src/resources/cluster/queries.ts | 537 ++++---- .../src/resources/cluster/router.spec.ts | 719 +++++------ .../src/resources/cluster/router.ts | 282 ++--- .../resources/environment/business.spec.ts | 778 ++++++------ .../src/resources/environment/business.ts | 582 ++++----- .../src/resources/environment/queries.ts | 153 ++- .../src/resources/environment/router.spec.ts | 956 ++++++-------- .../src/resources/environment/router.ts | 237 ++-- .../old-server/src/resources/index.ts | 217 +--- .../src/resources/log/business.spec.ts | 89 +- .../old-server/src/resources/log/business.ts | 26 +- .../old-server/src/resources/log/queries.ts | 98 +- .../src/resources/log/router.spec.ts | 199 ++- .../old-server/src/resources/log/router.ts | 68 +- .../src/resources/project-member/business.ts | 149 +-- .../src/resources/project-member/queries.ts | 55 +- .../resources/project-member/router.spec.ts | 746 +++++------ .../src/resources/project-member/router.ts | 209 ++-- .../resources/project-role/business.spec.ts | 429 +++---- .../src/resources/project-role/business.ts | 142 +-- .../src/resources/project-role/queries.ts | 99 +- .../src/resources/project-role/router.spec.ts | 807 +++++------- .../src/resources/project-role/router.ts | 223 ++-- .../src/resources/project-service/business.ts | 185 ++- .../src/resources/project-service/queries.ts | 96 +- .../resources/project-service/router.spec.ts | 412 +++---- .../src/resources/project-service/router.ts | 115 +- .../src/resources/project/business.spec.ts | 845 ++++++------- .../src/resources/project/business.ts | 564 ++++----- .../src/resources/project/queries.ts | 545 ++++---- .../src/resources/project/router.spec.ts | 1097 +++++++---------- .../src/resources/project/router.ts | 447 +++---- .../old-server/src/resources/queries-index.ts | 28 +- .../src/resources/repository/business.ts | 236 ++-- .../src/resources/repository/queries.ts | 93 +- .../src/resources/repository/router.spec.ts | 1044 ++++++---------- .../src/resources/repository/router.ts | 331 ++--- .../resources/service-chain/business.spec.ts | 329 +++-- .../src/resources/service-chain/business.ts | 22 +- .../src/resources/service-chain/queries.ts | 74 +- .../resources/service-chain/router.spec.ts | 582 ++++----- .../src/resources/service-chain/router.ts | 183 ++- .../src/resources/service-monitor/business.ts | 6 +- .../resources/service-monitor/router.spec.ts | 174 ++- .../src/resources/service-monitor/router.ts | 93 +- .../src/resources/stage/business.spec.ts | 270 ++-- .../src/resources/stage/business.ts | 164 ++- .../old-server/src/resources/stage/queries.ts | 157 ++- .../src/resources/stage/router.spec.ts | 471 +++---- .../old-server/src/resources/stage/router.ts | 181 ++- .../resources/system/config/business.spec.ts | 43 +- .../src/resources/system/config/business.ts | 85 +- .../src/resources/system/config/queries.ts | 47 +- .../resources/system/config/router.spec.ts | 203 ++- .../src/resources/system/config/router.ts | 66 +- .../old-server/src/resources/system/index.ts | 2 +- .../src/resources/system/router.spec.ts | 46 +- .../old-server/src/resources/system/router.ts | 42 +- .../src/resources/system/settings/business.ts | 16 +- .../src/resources/system/settings/queries.ts | 29 +- .../resources/system/settings/router.spec.ts | 116 +- .../src/resources/system/settings/router.ts | 56 +- .../src/resources/user/business.spec.ts | 463 ++++--- .../old-server/src/resources/user/business.ts | 448 +++---- .../old-server/src/resources/user/queries.ts | 87 +- .../src/resources/user/router.spec.ts | 291 ++--- .../old-server/src/resources/user/router.ts | 138 +-- .../src/resources/user/tokens/business.ts | 96 +- .../src/resources/user/tokens/router.ts | 109 +- .../src/resources/zone/business.spec.ts | 266 ++-- .../old-server/src/resources/zone/business.ts | 167 +-- .../old-server/src/resources/zone/queries.ts | 35 +- .../src/resources/zone/router.spec.ts | 366 +++--- .../old-server/src/resources/zone/router.ts | 122 +- .../cpin-module/old-server/src/server.spec.ts | 92 +- .../src/cpin-module/old-server/src/server.ts | 193 +-- .../old-server/src/utils/business.ts | 75 +- .../old-server/src/utils/controller.ts | 349 +++--- .../old-server/src/utils/date.spec.ts | 23 +- .../cpin-module/old-server/src/utils/date.ts | 4 +- .../cpin-module/old-server/src/utils/env.ts | 76 +- .../old-server/src/utils/errors.ts | 58 +- .../old-server/src/utils/fastify.ts | 107 +- .../old-server/src/utils/hook-wrapper.spec.ts | 456 ++++--- .../old-server/src/utils/hook-wrapper.ts | 558 ++++----- .../src/utils/keycloak-utils.spec.ts | 77 +- .../old-server/src/utils/keycloak-utils.ts | 36 +- .../old-server/src/utils/keycloak.ts | 79 +- .../old-server/src/utils/logger.ts | 201 ++- .../cpin-module/old-server/src/utils/mocks.ts | 288 ++--- .../old-server/src/utils/plugins.ts | 15 +- .../old-server/src/utils/proxy.spec.ts | 306 +++-- .../cpin-module/old-server/src/utils/proxy.ts | 157 +-- .../src/utils/queries-tools.spec.ts | 84 +- .../old-server/src/utils/queries-tools.ts | 11 +- .../old-server/src/utils/random.spec.ts | 298 +++-- .../src/cpin-module/old-server/vite.config.ts | 25 +- .../src/cpin-module/old-server/vitest-init.ts | 22 +- .../cpin-module/old-server/vitest.config.ts | 64 +- 129 files changed, 11739 insertions(+), 15948 deletions(-) create mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql diff --git a/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts b/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts index 1823401ec..057121c97 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts @@ -1,9 +1,9 @@ -import path from 'node:path'; -import { defineConfig } from 'prisma/config'; +import path from 'node:path' +import { defineConfig } from 'prisma/config' export default defineConfig({ - schema: path.join('src', 'prisma', 'schema'), - migrations: { - path: path.join('src', 'prisma', 'migrations'), - }, -}); + schema: path.join('src', 'prisma', 'schema'), + migrations: { + path: path.join('src', 'prisma', 'migrations'), + }, +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts index 265c128bc..2a871fd47 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts @@ -1,14 +1,14 @@ -import type { PrismaClient } from '@prisma/client'; -import { beforeEach, vi } from 'vitest'; -import { mockDeep, mockReset } from 'vitest-mock-extended'; +import type { PrismaClient } from '@prisma/client' +import { beforeEach, vi } from 'vitest' +import { mockDeep, mockReset } from 'vitest-mock-extended' -vi.mock('../prisma'); +vi.mock('../prisma') -const prisma = mockDeep(); +const prisma = mockDeep() beforeEach(() => { - // reset les mocks - mockReset(prisma); -}); + // reset les mocks + mockReset(prisma) +}) -export default prisma; +export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts index e9fa3359d..50939fd6f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts @@ -1,34 +1,34 @@ -import { beforeEach, vi } from 'vitest'; -import { mockDeep, mockReset } from 'vitest-mock-extended'; +import { beforeEach, vi } from 'vitest' +import { mockDeep, mockReset } from 'vitest-mock-extended' -vi.mock('../utils/hook-wrapper'); +vi.mock('../utils/hook-wrapper') export const hook = { - cluster: { - delete: vi.fn(), - upsert: vi.fn(), - }, - misc: { - checkServices: vi.fn(), - syncRepository: vi.fn(), - }, - project: { - upsert: vi.fn(), - delete: vi.fn(), - getSecrets: vi.fn(), - }, - user: { - retrieveUserByEmail: vi.fn(), - }, - zone: { - delete: vi.fn(), - upsert: vi.fn(), - }, -} as const; + cluster: { + delete: vi.fn(), + upsert: vi.fn(), + }, + misc: { + checkServices: vi.fn(), + syncRepository: vi.fn(), + }, + project: { + upsert: vi.fn(), + delete: vi.fn(), + getSecrets: vi.fn(), + }, + user: { + retrieveUserByEmail: vi.fn(), + }, + zone: { + delete: vi.fn(), + upsert: vi.fn(), + }, +} as const -const hookMock = mockDeep(); +const hookMock = mockDeep() beforeEach(() => { - // reset les mocks - mockReset(hookMock); -}); + // reset les mocks + mockReset(hookMock) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts index 081d636a6..e71dad6c1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts @@ -1,24 +1,21 @@ -import { apiPrefix } from '@cpn-console/shared'; -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { apiPrefix } from '@cpn-console/shared' +import app from './app' +import { getRandomRequestor, setRequestor } from './utils/mocks' -import app from './app'; -import { getRandomRequestor, setRequestor } from './utils/mocks'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('./utils/mocks')).mockSessionPlugin, -); +vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks')).mockSessionPlugin) describe('app', () => { - beforeEach(() => { - setRequestor(getRandomRequestor()); - }); - afterAll(async () => { - await app.close(); - }); + beforeEach(() => { + setRequestor(getRandomRequestor()) + }) + afterAll(async () => { + await app.close() + }) - it('should respond 404 on unknown route', async () => { - const response = await app.inject().get(`${apiPrefix}/miss`); - expect(response.statusCode).toBe(404); - }); -}); + it('should respond 404 on unknown route', async () => { + const response = await app.inject() + .get(`${apiPrefix}/miss`) + expect(response.statusCode).toBe(404) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts index 04942dee2..7b06c1608 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts @@ -1,90 +1,59 @@ -import { apiPrefix, getContract } from '@cpn-console/shared'; -import fastifyCookie from '@fastify/cookie'; -import helmet from '@fastify/helmet'; -import fastifySession from '@fastify/session'; -import fastifySwagger from '@fastify/swagger'; -import fastifySwaggerUi from '@fastify/swagger-ui'; -import { Injectable } from '@nestjs/common'; -import { initServer } from '@ts-rest/fastify'; -import { generateOpenApi } from '@ts-rest/open-api'; -import type { FastifyInstance, FastifyRequest } from 'fastify'; -import fastify from 'fastify'; -import keycloak from 'fastify-keycloak-adapter'; +import type { FastifyRequest } from 'fastify' +import fastify from 'fastify' +import helmet from '@fastify/helmet' +import keycloak from 'fastify-keycloak-adapter' +import fastifySession from '@fastify/session' +import fastifyCookie from '@fastify/cookie' +import fastifySwagger from '@fastify/swagger' +import fastifySwaggerUi from '@fastify/swagger-ui' +import { initServer } from '@ts-rest/fastify' +import { generateOpenApi } from '@ts-rest/open-api' +import { apiPrefix, getContract } from '@cpn-console/shared' +import { isDev, isInt, isTest } from './utils/env' +import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify' +import { apiRouter } from './resources/index' +import { keycloakConf, sessionConf } from './utils/keycloak' +import type { CustomLogger } from './utils/logger' +import { log } from './utils/logger' -import { ResourcesService } from './resources/index'; -import { isDev, isInt, isTest } from './utils/env'; -import { FastifyService } from './utils/fastify'; -import { keycloakConf, sessionConf } from './utils/keycloak'; -import type { CustomLogger } from './utils/logger'; -import { LoggerService } from './utils/logger'; +export const serverInstance: ReturnType = initServer() -@Injectable() -export class AppService { - constructor( - private readonly loggerService: LoggerService, - private readonly fastifyService: FastifyService, - private readonly resourcesService: ResourcesService, - ) {} +const openApiDocument = generateOpenApi(await getContract(), swaggerConf, { setOperationId: true }) - serverInstance: ReturnType = initServer(); - - app: FastifyInstance; - logger: any; +const app = fastify(fastifyConf) + .register(helmet, () => ({ + contentSecurityPolicy: !(isInt || isDev || isTest), + })) + .register(fastifyCookie) + .register(fastifySession, sessionConf) + // @ts-ignore + .register(keycloak, keycloakConf) + .register(fastifySwagger, { transformObject: () => openApiDocument }) + .register(fastifySwaggerUi, swaggerUiConf) + .register(apiRouter()) + .addHook('onRoute', (opts) => { + if (opts.path === `${apiPrefix}/healthz`) { + opts.logLevel = 'silent' + } + }) + .setErrorHandler((error: Error, req: FastifyRequest, reply) => { + const statusCode = 500 + // @ts-ignore vérifier l'objet + const message = error.description || error.message + reply.status(statusCode).send({ status: statusCode, error: message, stack: error.stack }) + log('info', { reqId: req.id, error }) + }) + .addHook('onResponse', (req, res) => { + if (res.statusCode < 400) { + req.log.info({ status: res.statusCode, userId: req.session?.user?.id }) + } else if (res.statusCode < 500) { + req.log.warn({ status: res.statusCode, userId: req.session?.user?.id }) + } else { + req.log.error({ status: res.statusCode, userId: req.session?.user?.id }) + } + }) - async init() { - const contract = await getContract(); - this.app = fastify(this.fastifyService.fastifyConf) - .register(helmet, () => ({ - contentSecurityPolicy: !(isInt || isDev || isTest), - })) - .register(fastifyCookie) - .register(fastifySession, sessionConf) - // @ts-ignore - .register(keycloak, keycloakConf) - .register(fastifySwagger, { - transformObject: () => - generateOpenApi(contract, this.fastifyService.swaggerConf, { - setOperationId: true, - }), - }) - .register(fastifySwaggerUi, this.fastifyService.swaggerUiConf) - .register(this.resourcesService.apiRouter()) - .addHook('onRoute', (opts) => { - if (opts.path === `${apiPrefix}/healthz`) { - opts.logLevel = 'silent'; - } - }) - .setErrorHandler((error: Error, req: FastifyRequest, reply) => { - const statusCode = 500; - // @ts-ignore vérifier l'objet - const message = error.description || error.message; - reply.status(statusCode).send({ - status: statusCode, - error: message, - stack: error.stack, - }); - this.loggerService.log('info', { reqId: req.id, error }); - }) - .addHook('onResponse', (req, res) => { - if (res.statusCode < 400) { - req.log.info({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } else if (res.statusCode < 500) { - req.log.warn({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } else { - req.log.error({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } - }); - this.logger = this.app.log as CustomLogger; +await app.ready() - await this.app.ready(); - } -} +export const logger = app.log as CustomLogger +export default app diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts index 004275b7f..c86eaec64 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts @@ -1,81 +1,61 @@ -import { PrismaClientInitializationError } from '@prisma/client/runtime/library'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import prisma from './__mocks__/prisma'; -import app, { logger } from './app'; -import { getConnection } from './connect'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('./utils/mocks')).mockSessionPlugin, -); -vi.mock('@old-server/resources/queries-index'); -vi.mock('./models/log', () => getModel('getLogModel')); -vi.mock('./models/repository', () => getModel('getRepositoryModel')); -vi.mock('./models/permission', () => getModel('getPermissionModel')); -vi.mock('./models/environment', () => getModel('getEnvironmentModel')); -vi.mock('./models/project', () => getModel('getProjectModel')); -vi.mock('./models/user', () => getModel('getUserModel')); -vi.mock('./models/users-projects', () => getModel('getRolesModel')); -vi.mock('./models/zone', () => getModel('getZoneModel')); -vi.mock('./prisma'); - -vi.spyOn(app, 'listen'); -vi.spyOn(logger, 'info'); -vi.spyOn(logger, 'warn'); -vi.spyOn(logger, 'error'); -vi.spyOn(logger, 'debug'); +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PrismaClientInitializationError } from '@prisma/client/runtime/library' +import prisma from './__mocks__/prisma' +import app, { logger } from './app' +import { getConnection } from './connect' + +vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks')).mockSessionPlugin) +vi.mock('@old-server/resources/queries-index') +vi.mock('./models/log', () => getModel('getLogModel')) +vi.mock('./models/repository', () => getModel('getRepositoryModel')) +vi.mock('./models/permission', () => getModel('getPermissionModel')) +vi.mock('./models/environment', () => getModel('getEnvironmentModel')) +vi.mock('./models/project', () => getModel('getProjectModel')) +vi.mock('./models/user', () => getModel('getUserModel')) +vi.mock('./models/users-projects', () => getModel('getRolesModel')) +vi.mock('./models/zone', () => getModel('getZoneModel')) +vi.mock('./prisma') + +vi.spyOn(app, 'listen') +vi.spyOn(logger, 'info') +vi.spyOn(logger, 'warn') +vi.spyOn(logger, 'error') +vi.spyOn(logger, 'debug') function getModel(modelName) { - return { - [modelName]: vi.fn(() => ({ - sync: vi.fn(), - hasMany: vi.fn(), - belongsTo: vi.fn(), - belongsToMany: vi.fn(), - })), - }; + return { + [modelName]: vi.fn(() => ({ + sync: vi.fn(), + hasMany: vi.fn(), + belongsTo: vi.fn(), + belongsToMany: vi.fn(), + })), + } } describe('connect', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should connect to postgres', async () => { - await getConnection(); - - expect(logger.info.mock.calls).toHaveLength(2); - expect(logger.info.mock.calls).toContainEqual([ - `Trying to connect to Postgres with: ${process.env.DB_URL}`, - ]); - expect(logger.info.mock.calls).toContainEqual([ - 'Connected to Postgres!', - ]); - }); - - it('should fail to connect once, then connect to postgres', async () => { - const errorToCatch = new PrismaClientInitializationError( - 'Failed to connect', - '2.19.0', - 'P1001', - ); - - prisma.$connect.mockRejectedValueOnce(errorToCatch); - await getConnection(); - - expect(logger.info.mock.calls).toHaveLength(5); - expect(logger.info.mock.calls).toContainEqual([ - `Trying to connect to Postgres with: ${process.env.DB_URL}`, - ]); - expect(logger.info.mock.calls).toContainEqual([ - 'Could not connect to Postgres: Failed to connect', - ]); - expect(logger.info.mock.calls).toContainEqual([ - 'Retrying (4 tries left)', - ]); - expect(logger.info.mock.calls).toContainEqual([ - 'Connected to Postgres!', - ]); - }); -}); + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should connect to postgres', async () => { + await getConnection() + + expect(logger.info.mock.calls).toHaveLength(2) + expect(logger.info.mock.calls).toContainEqual([`Trying to connect to Postgres with: ${process.env.DB_URL}`]) + expect(logger.info.mock.calls).toContainEqual(['Connected to Postgres!']) + }) + + it('should fail to connect once, then connect to postgres', async () => { + const errorToCatch = new PrismaClientInitializationError('Failed to connect', '2.19.0', 'P1001') + + prisma.$connect.mockRejectedValueOnce(errorToCatch) + await getConnection() + + expect(logger.info.mock.calls).toHaveLength(5) + expect(logger.info.mock.calls).toContainEqual([`Trying to connect to Postgres with: ${process.env.DB_URL}`]) + expect(logger.info.mock.calls).toContainEqual(['Could not connect to Postgres: Failed to connect']) + expect(logger.info.mock.calls).toContainEqual(['Retrying (4 tries left)']) + expect(logger.info.mock.calls).toContainEqual(['Connected to Postgres!']) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts index acdd8f771..ebd23ad3c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts @@ -1,61 +1,52 @@ -import { Injectable } from '@nestjs/common'; -import { setTimeout } from 'node:timers/promises'; - -import { AppService } from './app'; -import prisma from './prisma'; -import { dbUrl, isCI, isDev, isTest } from './utils/env'; - -@Injectable() -export class ConnectionService { - constructor(private readonly appService: AppService) {} - - closingConnections = false; - - async getConnection(triesLeft = 5): Promise { - if (this.closingConnections || triesLeft <= 0) { - throw new Error('Unable to connect to Postgres server'); - } - triesLeft--; - - try { - if (isDev || isTest || isCI) { - this.appService.logger.info( - `Trying to connect to Postgres with: ${dbUrl}`, - ); - } - await prisma.$connect(); - - this.appService.logger.info('Connected to Postgres!'); - } catch (error) { - if (triesLeft > 0) { - this.appService.logger.error(error); - this.appService.logger.info( - `Could not connect to Postgres: ${error.message}`, - ); - this.appService.logger.info( - `Retrying (${triesLeft} tries left)`, - ); - await setTimeout(isTest || isCI ? 1000 : 10000); - return this.getConnection(triesLeft); - } - - this.appService.logger.info( - `Could not connect to Postgres: ${error.message}`, - ); - this.appService.logger.info('Out of retries'); - error.message = `Out of retries, last error: ${error.message}`; - throw error; - } +import { setTimeout } from 'node:timers/promises' +import prisma from './prisma' +import { logger } from './app' +import { + dbUrl, + isCI, + isDev, + isTest, +} from './utils/env' + +const DELAY_BEFORE_RETRY = isTest || isCI ? 1000 : 10000 +let closingConnections = false + +export async function getConnection(triesLeft = 5): Promise { + if (closingConnections || triesLeft <= 0) { + throw new Error('Unable to connect to Postgres server') + } + triesLeft-- + + try { + if (isDev || isTest || isCI) { + logger.info(`Trying to connect to Postgres with: ${dbUrl}`) } - - async closeConnections() { - this.closingConnections = true; - try { - await prisma.$disconnect(); - } catch (error) { - this.appService.logger.error(error); - } finally { - this.closingConnections = false; - } + await prisma.$connect() + + logger.info('Connected to Postgres!') + } catch (error) { + if (triesLeft > 0) { + logger.error(error) + logger.info(`Could not connect to Postgres: ${error.message}`) + logger.info(`Retrying (${triesLeft} tries left)`) + await setTimeout(DELAY_BEFORE_RETRY) + return getConnection(triesLeft) } + + logger.info(`Could not connect to Postgres: ${error.message}`) + logger.info('Out of retries') + error.message = `Out of retries, last error: ${error.message}` + throw error + } +} + +export async function closeConnections() { + closingConnections = true + try { + await prisma.$disconnect() + } catch (error) { + logger.error(error) + } finally { + closingConnections = false + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts index 5679aa56c..c12f4f9d0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts @@ -1,38 +1,28 @@ // @ts-nocheck + /** * How to use ? * npx vite-node src/init/db/dump.ts * format ./data.ts with linter * cut/paste to packages/test-utils/src/imports/data.ts */ -import prisma from '@old-server/prisma'; -import { Prisma } from '@prisma/client'; -import { writeFileSync } from 'node:fs'; -import { - associations, - manyToManyRelation, - modelKeys, - models, - resourceListToDict, -} from './utils'; +import { writeFileSync } from 'node:fs' +import { Prisma } from '@prisma/client' +import { associations, manyToManyRelation, modelKeys, models, resourceListToDict } from './utils' +import prisma from '@old-server/prisma' -const Models = resourceListToDict(Prisma.dmmf.datamodel.models); +const Models = resourceListToDict(Prisma.dmmf.datamodel.models) for (const modelKey of modelKeys) { - const modelDatas = await prisma[modelKey].findMany(); - models[modelKey] = modelDatas; + const modelDatas = await prisma[modelKey].findMany() + models[modelKey] = modelDatas } for (const [model, targetModel, relationKey] of manyToManyRelation) { - const modelKey = model.slice(0, 1).toLocaleLowerCase() + model.slice(1); - const modelDatas = await prisma[modelKey].findMany({ - select: { - [Models[model].id]: true, - [relationKey]: { select: { [Models[targetModel].id]: true } }, - }, - }); - associations.push([modelKey, modelDatas]); + const modelKey = model.slice(0, 1).toLocaleLowerCase() + model.slice(1) + const modelDatas = await prisma[modelKey].findMany({ select: { [Models[model].id]: true, [relationKey]: { select: { [Models[targetModel].id]: true } } } }) + associations.push([modelKey, modelDatas]) } -const a = JSON.stringify({ ...models, associations }, null, 2); +const a = JSON.stringify({ ...models, associations }, null, 2) -writeFileSync('./data', `export const data = ${a}`); +writeFileSync('./data', `export const data = ${a}`) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts index a7b921eb4..edb03b728 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts @@ -1,66 +1,51 @@ -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import prisma from '@old-server/prisma'; - -import { modelKeys } from './utils'; +import { modelKeys } from './utils' +import { logger } from '@old-server/app' +import prisma from '@old-server/prisma' type ExtractKeysWithFields = { - [K in keyof T]: T[K] extends { fields: any } ? K : never; -}[keyof T]; + [K in keyof T]: T[K] extends { fields: any } ? K : never +}[keyof T] -type Models = ExtractKeysWithFields; +type Models = ExtractKeysWithFields type Imports = Partial> & { - associations: [Models, any[]]; -}; - -@Injectable() -export class InitDBService { - constructor(private readonly appService: AppService) {} + associations: [Models, any[]] +} - async initDb(data: Imports) { - const dataStringified = JSON.stringify(data); - const dataParsed = JSON.parse(dataStringified, (key, value) => { - try { - if (['permissions', 'everyonePerms'].includes(key)) { - return BigInt(value.slice(0, value.length - 1)); - } - } catch (_error) { - return value; - } - return value; - }); - this.appService.logger.info('Drop tables'); - for (const modelKey of modelKeys.toReversed()) { - // @ts-ignore - await prisma[modelKey].deleteMany(); - } - this.appService.logger.info('Import models'); - for (const modelKey of modelKeys) { - // @ts-ignore - await prisma[modelKey].createMany({ data: dataParsed[modelKey] }); - } - this.appService.logger.info('Import associations'); - for (const [modelKey, rows] of dataParsed.associations) { - for (const row of rows) { - const idKey = 'id'; - const connectKeys = Object.keys(row).filter( - (key) => key !== idKey, - ); - const dataConnects = connectKeys.reduce( - (acc, curr) => { - acc[curr] = { connect: row[curr] }; - return acc; - }, - {} as Record, - ); - // @ts-ignore - await prisma[modelKey].update({ - where: { id: row.id }, - data: dataConnects, - }); - } - } - this.appService.logger.info('End import'); +export async function initDb(data: Imports) { + const dataStringified = JSON.stringify(data) + const dataParsed = JSON.parse(dataStringified, (key, value) => { + try { + if (['permissions', 'everyonePerms'].includes(key)) { + return BigInt(value.slice(0, value.length - 1)) + } + } catch (_error) { + return value + } + return value + }) + logger.info('Drop tables') + for (const modelKey of modelKeys.toReversed()) { + // @ts-ignore + await prisma[modelKey].deleteMany() + } + logger.info('Import models') + for (const modelKey of modelKeys) { + // @ts-ignore + await prisma[modelKey].createMany({ data: dataParsed[modelKey] }) + } + logger.info('Import associations') + for (const [modelKey, rows] of dataParsed.associations) { + for (const row of rows) { + const idKey = 'id' + const connectKeys = Object.keys(row).filter(key => key !== idKey) + const dataConnects = connectKeys.reduce((acc, curr) => { + acc[curr] = { connect: row[curr] } + return acc + }, {} as Record) + // @ts-ignore + await prisma[modelKey].update({ where: { id: row.id }, data: dataConnects }) } + } + logger.info('End import') } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts index 50cce2c10..4377e07a1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts @@ -1,53 +1,52 @@ -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest' +import prisma from '../../__mocks__/prisma' +import { modelKeys, moveBefore, resourceListToDict } from './utils' -import prisma from '../../__mocks__/prisma'; -import { modelKeys, moveBefore, resourceListToDict } from './utils'; - -vi.mock('fs', () => ({ writeFileSync: vi.fn() })); +vi.mock('fs', () => ({ writeFileSync: vi.fn() })) for (const modelKey of modelKeys) { - prisma[modelKey].findMany.mockResolvedValue([]); + prisma[modelKey].findMany.mockResolvedValue([]) } describe('test moveBefore', () => { - it('should be moved', () => { - const arr = ['a', 'b', 'c']; - const arrSorted = moveBefore(arr, 'c', 'b'); - expect(arrSorted).toEqual(['a', 'c', 'b']); - - const arrSorted2 = moveBefore(arr, 'c', 'a'); - expect(arrSorted2).toEqual(['c', 'a', 'b']); - }); - it('should not be moved', () => { - const arr = ['a', 'b', 'c']; - const arrSorted = moveBefore(arr, 'b', 'c'); - expect(arrSorted).toEqual(false); - - const arrSorted2 = moveBefore(arr, 'a', 'c'); - expect(arrSorted2).toEqual(false); - - const arrSorted3 = moveBefore(arr, 'c', 'c'); - expect(arrSorted3).toEqual(false); - }); -}); + it('should be moved', () => { + const arr = ['a', 'b', 'c'] + const arrSorted = moveBefore(arr, 'c', 'b') + expect(arrSorted).toEqual(['a', 'c', 'b']) + + const arrSorted2 = moveBefore(arr, 'c', 'a') + expect(arrSorted2).toEqual(['c', 'a', 'b']) + }) + it('should not be moved', () => { + const arr = ['a', 'b', 'c'] + const arrSorted = moveBefore(arr, 'b', 'c') + expect(arrSorted).toEqual(false) + + const arrSorted2 = moveBefore(arr, 'a', 'c') + expect(arrSorted2).toEqual(false) + + const arrSorted3 = moveBefore(arr, 'c', 'c') + expect(arrSorted3).toEqual(false) + }) +}) it('test resourceListToDict (by name)', () => { - const list = [ - { name: 'a', value: 1 }, - { name: 'b', value: 2 }, - { name: 'c', value: 3 }, - ]; - const dict = resourceListToDict(list); - expect(dict).toEqual({ - a: { name: 'a', value: 1 }, - b: { name: 'b', value: 2 }, - c: { name: 'c', value: 3 }, - }); -}); + const list = [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 }, + { name: 'c', value: 3 }, + ] + const dict = resourceListToDict(list) + expect(dict).toEqual({ + a: { name: 'a', value: 1 }, + b: { name: 'b', value: 2 }, + c: { name: 'c', value: 3 }, + }) +}) it('stringify bigint', () => { - const list = { name: 'a', value: 1n }; + const list = { name: 'a', value: 1n } - const dict = JSON.stringify(list); + const dict = JSON.stringify(list) - expect(dict).toEqual('{"name":"a","value":"1n"}'); -}); + expect(dict).toEqual('{"name":"a","value":"1n"}') +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts index 941a10fa3..95924751a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts @@ -1,107 +1,85 @@ // @ts-nocheck -import { Prisma } from '@prisma/client'; +import { Prisma } from '@prisma/client' // eslint-disable-next-line no-extend-native BigInt.prototype.toJSON = function () { - return `${this.toString()}n`; -}; + return `${this.toString()}n` +} -export type ResourceByName< - T extends { - name: string; - }, -> = Record; -export function resourceListToDict( - resList: Array, -): ResourceByName { - return resList.reduce( - (acc, curr) => { - return { - ...acc, - [curr.name]: curr, - }; - }, - {} as ResourceByName, - ); +export type ResourceByName = Record +export function resourceListToDict(resList: Array): ResourceByName { + return resList.reduce((acc, curr) => { + return { + ...acc, + [curr.name]: curr, + } + }, {} as ResourceByName) } // @ts-ignore -const Models = resourceListToDict(Prisma.dmmf.datamodel.models); -let ModelsNames = Object.keys(Models); -let ModelsOrder = [...ModelsNames]; +const Models = resourceListToDict(Prisma.dmmf.datamodel.models) +let ModelsNames = Object.keys(Models) +let ModelsOrder = [...ModelsNames] -export function moveBefore( - arr: T, - toMove: T[number], - ref: T[number], -): T | false { - const iref = arr.indexOf(ref); - const moveref = arr.indexOf(toMove); - if (moveref <= iref) return false; - return [ - ...arr.slice(0, iref), - arr[moveref], - ...arr.slice(iref, moveref), - ...arr.slice(moveref + 1), - ] as T; +export function moveBefore(arr: T, toMove: T[number], ref: T[number]): T | false { + const iref = arr.indexOf(ref) + const moveref = arr.indexOf(toMove) + if (moveref <= iref) return false + return [ + ...arr.slice(0, iref), + arr[moveref], + ...arr.slice(iref, moveref), + ...arr.slice(moveref + 1), + ] as T } -export const manyToManyRelation: [string, string, string][] = []; +export const manyToManyRelation: [string, string, string][] = [] function sort() { - let hasChanged = false; - for (const model of ModelsNames) { - for (const field of Models[model].fields) { - if (field.isId) Models[model].id = field.name; - if (field.type in Models) { - const relationField = Models[field.type].fields.find( - ({ type }) => type === model, - ); - if (!relationField) - throw new Error('unable to find matching model'); - if ( - (relationField.isRequired && - field.isRequired && - !relationField.isList) || - (relationField.isRequired && !field.isRequired) - ) { - const moveRes = moveBefore(ModelsOrder, model, field.type); - if (moveRes) { - hasChanged = true; - ModelsOrder = moveRes; - } - } - if ( - field.isList && - relationField.isList && - !manyToManyRelation.find( - (test) => - (test[0] === model && test[1] === field.type) || - (test[0] === field.type && test[1] === model), - ) - ) { - manyToManyRelation.push([model, field.type, field.name]); - } - } + let hasChanged = false + for (const model of ModelsNames) { + for (const field of Models[model].fields) { + if (field.isId) Models[model].id = field.name + if (field.type in Models) { + const relationField = Models[field.type].fields.find(({ type }) => type === model) + if (!relationField) throw new Error('unable to find matching model') + if ( + (relationField.isRequired && field.isRequired && !relationField.isList) + || (relationField.isRequired && !field.isRequired) + ) { + const moveRes = moveBefore(ModelsOrder, model, field.type) + if (moveRes) { + hasChanged = true + ModelsOrder = moveRes + } + } + if ( + field.isList && relationField.isList + && !manyToManyRelation.find(test => + (test[0] === model && test[1] === field.type) || (test[0] === field.type && test[1] === model)) + ) { + manyToManyRelation.push([model, field.type, field.name]) } + } } - ModelsNames = ModelsOrder; - if (hasChanged) sort(); + } + ModelsNames = ModelsOrder + if (hasChanged) sort() } -sort(); +sort() // special case to study -const logUserCase = moveBefore(ModelsOrder, 'User', 'Log'); +const logUserCase = moveBefore(ModelsOrder, 'User', 'Log') if (logUserCase) { - ModelsOrder = logUserCase; + ModelsOrder = logUserCase } -const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log'); +const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log') if (logProjectCase) { - ModelsOrder = logProjectCase; + ModelsOrder = logProjectCase } -export const models: Record = {}; -export const associations: Record = []; -export const modelKeys = ModelsOrder.map( - (model) => model.slice(0, 1).toLocaleLowerCase() + model.slice(1), -); +export const models: Record = {} +export const associations: Record = [] +export const modelKeys = ModelsOrder.map(model => model.slice(0, 1).toLocaleLowerCase() + model.slice(1)) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts index 265c128bc..2a871fd47 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts @@ -1,14 +1,14 @@ -import type { PrismaClient } from '@prisma/client'; -import { beforeEach, vi } from 'vitest'; -import { mockDeep, mockReset } from 'vitest-mock-extended'; +import type { PrismaClient } from '@prisma/client' +import { beforeEach, vi } from 'vitest' +import { mockDeep, mockReset } from 'vitest-mock-extended' -vi.mock('../prisma'); +vi.mock('../prisma') -const prisma = mockDeep(); +const prisma = mockDeep() beforeEach(() => { - // reset les mocks - mockReset(prisma); -}); + // reset les mocks + mockReset(prisma) +}) -export default prisma; +export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts index 2fd1aab81..3e9556625 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts @@ -1,24 +1,24 @@ -import type { User } from '@cpn-console/test-utils'; -import fp from 'fastify-plugin'; +import fp from 'fastify-plugin' +import type { User } from '@cpn-console/test-utils' -let requestor: User; +let requestor: User export function setRequestor(user: User) { - requestor = user; + requestor = user } export function getRequestor() { - return requestor; + return requestor } export async function mockSessionPlugin() { - const sessionPlugin = (app, opt, next) => { - app.addHook('onRequest', (req, res, next) => { - req.session = { user: getRequestor() }; - next(); - }); - next(); - }; + const sessionPlugin = (app, opt, next) => { + app.addHook('onRequest', (req, res, next) => { + req.session = { user: getRequestor() } + next() + }) + next() + } - return { default: fp(sessionPlugin) }; + return { default: fp(sessionPlugin) } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts index 5d75de9d1..eef052482 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts @@ -1,60 +1,46 @@ -import { plugin as argo } from '@cpn-console/argocd-plugin'; -import { plugin as gitlab } from '@cpn-console/gitlab-plugin'; -import { plugin as harbor } from '@cpn-console/harbor-plugin'; -import { type Plugin, pluginManager } from '@cpn-console/hooks'; -import { plugin as keycloak } from '@cpn-console/keycloak-plugin'; -import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin'; -import { plugin as nexus } from '@cpn-console/nexus-plugin'; -import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin'; -import { plugin as vault } from '@cpn-console/vault-plugin'; -import { Injectable } from '@nestjs/common'; -import { readdirSync, statSync } from 'node:fs'; +import { readdirSync, statSync } from 'node:fs' +import { type Plugin, pluginManager } from '@cpn-console/hooks' +import { plugin as argo } from '@cpn-console/argocd-plugin' +import { plugin as gitlab } from '@cpn-console/gitlab-plugin' +import { plugin as harbor } from '@cpn-console/harbor-plugin' +import { plugin as keycloak } from '@cpn-console/keycloak-plugin' +import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin' +import { plugin as nexus } from '@cpn-console/nexus-plugin' +import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin' +import { plugin as vault } from '@cpn-console/vault-plugin' +import { pluginManagerOptions } from './utils/plugins' +import { pluginsDir } from './utils/env' -import { pluginsDir } from './utils/env'; -import { pluginManagerOptions } from './utils/plugins'; +export async function initPm() { + const pm = pluginManager(pluginManagerOptions) + pm.register(argo) + pm.register(gitlab) + pm.register(harbor) + pm.register(keycloak) + pm.register(kubernetes) + pm.register(nexus) + pm.register(sonarqube) + pm.register(vault) -@Injectable() -export class PluginService { - async initPm() { - const pm = pluginManager(pluginManagerOptions); - pm.register(argo); - pm.register(gitlab); - pm.register(harbor); - pm.register(keycloak); - pm.register(kubernetes); - pm.register(nexus); - pm.register(sonarqube); - pm.register(vault); - - if ( - !statSync(pluginsDir, { - throwIfNoEntry: false, - }) - ) { - return pm; - } - for (const dirName of readdirSync(pluginsDir)) { - const moduleAbsPath = `${pluginsDir}/${dirName}`; - try { - statSync(`${moduleAbsPath}/package.json`); - const pkg = await import(`${moduleAbsPath}/package.json`, { - with: { type: 'json' }, - }); - const entrypoint = pkg.default.module || pkg.default.main; - if (!entrypoint) - throw new Error( - `No entrypoint found in package.json : ${pkg.default.name}`, - ); - const { plugin } = (await import( - `${moduleAbsPath}/${entrypoint}` - )) as { plugin: Plugin }; - pm.register(plugin); - } catch (error) { - console.error(`Could not import module ${moduleAbsPath}`); - console.error(error.stack); - } - } - - return pm; + if (!statSync(pluginsDir, { + throwIfNoEntry: false, + })) { + return pm + } + for (const dirName of readdirSync(pluginsDir)) { + const moduleAbsPath = `${pluginsDir}/${dirName}` + try { + statSync(`${moduleAbsPath}/package.json`) + const pkg = await import(`${moduleAbsPath}/package.json`, { with: { type: 'json' } }) + const entrypoint = pkg.default.module || pkg.default.main + if (!entrypoint) throw new Error(`No entrypoint found in package.json : ${pkg.default.name}`) + const { plugin } = await import(`${moduleAbsPath}/${entrypoint}`) as { plugin: Plugin } + pm.register(plugin) + } catch (error) { + console.error(`Could not import module ${moduleAbsPath}`) + console.error(error.stack) } + } + + return pm } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts index 7b20d355e..2e8f0cb4b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts @@ -1,75 +1,70 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getPreparedApp } from './prepare-app' +import { getConnection } from './connect' +import { initDb } from './init/db/index' +import app, { logger } from './app' -import app, { logger } from './app'; -import { getConnection } from './connect'; -import { initDb } from './init/db/index'; -import { getPreparedApp } from './prepare-app'; +vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks')).mockSessionPlugin) +vi.mock('./connect') +vi.mock('./index') +vi.mock('./utils/logger') +vi.mock('./init/db/index', () => ({ initDb: vi.fn() })) -vi.mock( - 'fastify-keycloak-adapter', - (await import('./utils/mocks')).mockSessionPlugin, -); -vi.mock('./connect'); -vi.mock('./index'); -vi.mock('./utils/logger'); -vi.mock('./init/db/index', () => ({ initDb: vi.fn() })); - -vi.spyOn(app, 'listen'); -vi.spyOn(logger, 'info'); -vi.spyOn(logger, 'warn'); -vi.spyOn(logger, 'error'); -vi.spyOn(logger, 'debug'); +vi.spyOn(app, 'listen') +vi.spyOn(logger, 'info') +vi.spyOn(logger, 'warn') +vi.spyOn(logger, 'error') +vi.spyOn(logger, 'debug') describe('server', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); + beforeEach(() => { + vi.clearAllMocks() + }) - it('should getConnection', async () => { - // const port = Math.round(Math.random() * 10000) + 1024 - await getPreparedApp().catch((err) => console.warn(err)); + it('should getConnection', async () => { + // const port = Math.round(Math.random() * 10000) + 1024 + await getPreparedApp().catch(err => console.warn(err)) - expect(getConnection).toHaveBeenCalledTimes(1); - expect(initDb.mock.calls).toHaveLength(1); - }); + expect(getConnection).toHaveBeenCalledTimes(1) + expect(initDb.mock.calls).toHaveLength(1) + }) - it('should throw an error on connection to DB', async () => { - const error = new Error('This is OK!'); - getConnection.mockRejectedValueOnce(error); + it('should throw an error on connection to DB', async () => { + const error = new Error('This is OK!') + getConnection.mockRejectedValueOnce(error) - let response; - await getPreparedApp().catch((err) => { - response = err; - }); + let response + await getPreparedApp() + .catch((err) => { response = err }) - expect(getConnection.mock.calls).toHaveLength(1); - expect(app.listen.mock.calls).toHaveLength(0); - expect(response).toMatchObject(error); - }); + expect(getConnection.mock.calls).toHaveLength(1) + expect(app.listen.mock.calls).toHaveLength(0) + expect(response).toMatchObject(error) + }) - it('should throw an error on initDb import if module is not found', async () => { - const error = new Error('Failed to load'); - initDb.mockRejectedValueOnce(error); + it('should throw an error on initDb import if module is not found', async () => { + const error = new Error('Failed to load') + initDb.mockRejectedValueOnce(error) - await getPreparedApp(); + await getPreparedApp() - expect(initDb.mock.calls).toHaveLength(1); - expect(logger.info.mock.calls).toHaveLength(3); - }); + expect(initDb.mock.calls).toHaveLength(1) + expect(logger.info.mock.calls).toHaveLength(3) + }) - it('should throw an error on initDb import', async () => { - const error = new Error('This is OK!'); - initDb.mockRejectedValueOnce(error); + it('should throw an error on initDb import', async () => { + const error = new Error('This is OK!') + initDb.mockRejectedValueOnce(error) - let response; - try { - await getPreparedApp(); - } catch (err) { - response = err; - } + let response + try { + await getPreparedApp() + } catch (err) { + response = err + } - expect(initDb.mock.calls).toHaveLength(1); - expect(logger.info.mock.calls).toHaveLength(2); - expect(response).toMatchObject(error); - }); -}); + expect(initDb.mock.calls).toHaveLength(1) + expect(logger.info.mock.calls).toHaveLength(2) + expect(response).toMatchObject(error) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts index eb9f99862..5be3a06ba 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts @@ -1,128 +1,106 @@ -import { Injectable } from '@nestjs/common'; -import { ProxyAgent, setGlobalDispatcher } from 'undici'; +import { rm } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { isCI, isDev, isDevSetup, isInt, isProd, isTest, port } from './utils/env' +import app, { logger } from './app' +import { getConnection } from './connect' +import { initDb } from './init/db/index' +import { initPm } from './plugins' -import { AppService } from './app'; -import { ConnectionService } from './connect'; -import { InitDBService } from './init/db'; -import { PluginService } from './plugins'; -import { isCI, isDev, isDevSetup, isProd, isTest } from './utils/env'; - -@Injectable() -export class PrepareAppService { - constructor( - private readonly appService: AppService, - private readonly connectionService: ConnectionService, - private readonly initDBService: InitDBService, - private readonly pluginService: PluginService, - - ) { - // Workaround because fetch isn't using http_proxy variables - // See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 - if (process.env.HTTP_PROXY) { - setGlobalDispatcher(new ProxyAgent(process.env.HTTP_PROXY)); - } - } - - async initializeDB(path: string) { - this.appService.logger.info('Starting init DB...'); - const { data } = await import(path); - await this.initDBService.initDb(data); - this.appService.logger.info('initDb invoked successfully'); - } +// Workaround because fetch isn't using http_proxy variables +// See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 +if (process.env.HTTP_PROXY) { + const Undici = await import('undici') + const ProxyAgent = Undici.ProxyAgent + const setGlobalDispatcher = Undici.setGlobalDispatcher + setGlobalDispatcher( + new ProxyAgent(process.env.HTTP_PROXY), + ) +} - async startServer() { - try { - await this.connectionService.getConnection(); - } catch (error) { - if (!(error instanceof Error)) return; - this.appService.logger.error(error.message); - throw error; - } +async function initializeDB(path: string) { + logger.info('Starting init DB...') + const { data } = await import(path) + await initDb(data) + logger.info('initDb invoked successfully') +} - this.pluginService.initPm(); +export async function startServer(defaultPort: number = (port ? +port : 8080)) { + try { + await getConnection() + } catch (error) { + if (!(error instanceof Error)) return + logger.error(error.message) + throw error + } - this.appService.logger.info('Reading init database file'); + initPm() - // try { - // const dataPath = - // isProd || isInt - // ? './init/db/imports/data' - // : '@cpn-console/test-utils/src/imports/data'; - // await initializeDB(dataPath); - // if (isProd && !isDevSetup) { - // this.appService.logger.info('Cleaning up imported data file...'); - // const __filename = fileURLToPath(import.meta.url); - // const __dirname = dirname(__filename); - // await rm(resolve(__dirname, dataPath)); - // this.appService.logger.info(`Successfully deleted '${dataPath}'`); - // } - // } catch (error) { - // if ( - // error.code === 'ERR_MODULE_NOT_FOUND' || - // error.message.includes('Failed to load') || - // error.message.includes('Cannot find module') - // ) { - // this.appService.logger.info('No initDb file, skipping'); - // } else { - // this.appService.logger.warn(error.message); - // throw error; - // } - // } + logger.info('Reading init database file') - this.appService.logger.debug({ - isDev, - isTest, - isCI, - isDevSetup, - isProd, - }); + try { + const dataPath = (isProd || isInt) + ? './init/db/imports/data' + : '@cpn-console/test-utils/src/imports/data' + await initializeDB(dataPath) + if (isProd && !isDevSetup) { + logger.info('Cleaning up imported data file...') + const __filename = fileURLToPath(import.meta.url) + const __dirname = dirname(__filename) + await rm(resolve(__dirname, dataPath)) + logger.info(`Successfully deleted '${dataPath}'`) + } + } catch (error) { + if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { + logger.info('No initDb file, skipping') + } else { + logger.warn(error.message) + throw error } + } - async getPreparedApp() { - try { - await this.connectionService.getConnection(); - } catch (error) { - this.appService.logger.error(error.message); - throw error; - } + try { + await app.listen({ host: '0.0.0.0', port: defaultPort ?? 8080 }) + } catch (error) { + logger.error(error) + process.exit(1) + } + logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) +} - this.pluginService.initPm(); +export async function getPreparedApp() { + try { + await getConnection() + } catch (error) { + logger.error(error.message) + throw error + } - this.appService.logger.info('Reading init database file'); + initPm() - // try { - // const dataPath = - // isProd || isInt - // ? './init/db/imports/data' - // : '@cpn-console/test-utils/src/imports/data'; - // await initializeDB(dataPath); - // if (isProd && !isDevSetup) { - // this.appService.logger.info('Cleaning up imported data file...'); - // const __filename = fileURLToPath(import.meta.url); - // const __dirname = dirname(__filename); - // await rm(resolve(__dirname, dataPath)); - // this.appService.logger.info(`Successfully deleted '${dataPath}'`); - // } - // } catch (error) { - // if ( - // error.code === 'ERR_MODULE_NOT_FOUND' || - // error.message.includes('Failed to load') || - // error.message.includes('Cannot find module') - // ) { - // this.appService.logger.info('No initDb file, skipping'); - // } else { - // this.appService.logger.warn(error.message); - // throw error; - // } - // } + logger.info('Reading init database file') - this.appService.logger.debug({ - isDev, - isTest, - isCI, - isDevSetup, - isProd, - }); - return this.appService.app; + try { + const dataPath = (isProd || isInt) + ? './init/db/imports/data' + : '@cpn-console/test-utils/src/imports/data' + await initializeDB(dataPath) + if (isProd && !isDevSetup) { + logger.info('Cleaning up imported data file...') + const __filename = fileURLToPath(import.meta.url) + const __dirname = dirname(__filename) + await rm(resolve(__dirname, dataPath)) + logger.info(`Successfully deleted '${dataPath}'`) } + } catch (error) { + if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { + logger.info('No initDb file, skipping') + } else { + logger.warn(error.message) + throw error + } + } + + logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) + return app } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts index 4e54f7a77..4590932b6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts @@ -1,5 +1,5 @@ -import { PrismaClient } from '@prisma/client'; +import { PrismaClient } from '@prisma/client' -const prisma = new PrismaClient(); +const prisma = new PrismaClient() -export default prisma; +export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql new file mode 100644 index 000000000..aadb6cdba --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Repository" ADD COLUMN "deployRevision" TEXT, +ADD COLUMN "deployPath" TEXT, +ADD COLUMN "helmValuesFiles" TEXT; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma index 44569a8fa..e76048675 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma @@ -24,6 +24,9 @@ model Repository { externalUserName String @default("") isInfra Boolean @default(false) isPrivate Boolean @default(false) + deployRevision String @default("") + deployPath String @default("") + helmValuesFiles String @default("") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts index 94a505bc2..7da66d6a4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts @@ -1,219 +1,183 @@ -import { faker } from '@faker-js/faker'; -import type { AdminRole, User } from '@prisma/client'; -import { describe, expect, it } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import { BadRequest400 } from '../../utils/errors'; -import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from './business'; +import { describe, expect, it } from 'vitest' +import type { AdminRole, User } from '@prisma/client' +import { faker } from '@faker-js/faker' +import prisma from '../../__mocks__/prisma' +import { BadRequest400 } from '../../utils/errors' +import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business' describe('test admin-role business', () => { - describe('listRoles', () => { - it('should stringify bigint', async () => { - const partialRole: Partial = { - permissions: 4n, - }; - - prisma.adminRole.findMany.mockResolvedValueOnce([partialRole]); - const response = await listRoles(); - expect(response).toEqual([{ permissions: '4' }]); - }); - }); - - describe('createRole', () => { - it('should create role with incremented position when position 0 is the highest', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 0, - }; - - prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole); - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]); - prisma.adminRole.create.mockResolvedValue(null); - await createRole({ name: 'test' }); - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ - data: { name: 'test', permissions: 0n, position: 1 }, - }); - }); - - it('should create role with incremented position with bigger position', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - }; - - prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole); - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]); - prisma.adminRole.create.mockResolvedValue(null); - await createRole({ name: 'test' }); - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ - data: { name: 'test', permissions: 0n, position: 51 }, - }); - }); - - it('should create role with incremented position with no role in db', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - }; - - prisma.adminRole.findFirst.mockResolvedValueOnce(undefined); - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]); - prisma.adminRole.create.mockResolvedValue(null); - await createRole({ name: 'test' }); - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ - data: { name: 'test', permissions: 0n, position: 0 }, - }); - }); - }); - describe('deleteRole', () => { - const roleId = faker.string.uuid(); - it('should delete role and remove id from concerned users', async () => { - const users = [ - { - adminRoleIds: [roleId], - id: faker.string.uuid(), - }, - { - adminRoleIds: [roleId, faker.string.uuid()], - id: faker.string.uuid(), - }, - ] as const satisfies Partial[]; - - prisma.user.findMany.mockResolvedValueOnce(users); - prisma.adminRole.findMany.mockResolvedValueOnce([]); - prisma.adminRole.create.mockResolvedValue(null); - await deleteRole(roleId); - - expect(prisma.user.update).toHaveBeenNthCalledWith(1, { - where: { id: users[0].id }, - data: { adminRoleIds: [] }, - }); - expect(prisma.user.update).toHaveBeenNthCalledWith(2, { - where: { id: users[1].id }, - data: { adminRoleIds: [users[1].adminRoleIds[1]] }, - }); - expect(prisma.adminRole.delete).toHaveBeenCalledWith({ - where: { id: roleId }, - }); - }); - }); - describe('countRolesMembers', () => { - it('should return aggregated role member counts', async () => { - const partialRoles = [ - { - id: faker.string.uuid(), - }, - { - id: faker.string.uuid(), - }, - ] as const satisfies Partial[]; - - const users = [ - { - adminRoleIds: [partialRoles[0].id, partialRoles[1].id], - }, - { - adminRoleIds: [partialRoles[1].id], - }, - ] as const satisfies Partial[]; - prisma.adminRole.findMany.mockResolvedValue(partialRoles); - prisma.user.findMany.mockResolvedValue(users); - - const response = await countRolesMembers(); - - expect(response).toEqual({ - [partialRoles[0].id]: 1, - [partialRoles[1].id]: 2, - }); - }); - }); - describe('patchRoles', () => { - const dbRoles: AdminRole[] = [ - { - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 0, - }, - { - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 1, - }, - ]; - - it('should do nothing', async () => { - prisma.adminRole.findMany.mockResolvedValue([]); - await patchRoles([]); - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0); - }); - - it('should return 400 if incoherent positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 1 }, - ]; - prisma.adminRole.findMany.mockResolvedValue(dbRoles); - - const response = await patchRoles(updateRoles); - - expect(response).instanceOf(BadRequest400); - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0); - }); - it('should return 400 if incoherent positions (missing roles)', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, position: 1 }, - ]; - prisma.adminRole.findMany.mockResolvedValue(dbRoles); - - const response = await patchRoles(updateRoles); - - expect(response).instanceOf(BadRequest400); - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0); - }); - it('should update positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 0 }, - ]; - prisma.adminRole.findMany.mockResolvedValue(dbRoles); - - await patchRoles(updateRoles); - - expect(prisma.adminRole.update).toHaveBeenCalledTimes(2); - }); - it('should update permissions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, permissions: '0' }, - ]; - prisma.adminRole.findMany.mockResolvedValue(dbRoles); - - await patchRoles(updateRoles); - - expect(prisma.adminRole.update).toHaveBeenCalledTimes(1); - expect(prisma.adminRole.update).toHaveBeenCalledWith({ - data: { - name: dbRoles[1].name, - oidcGroup: dbRoles[1].oidcGroup, - permissions: 0n, - position: 1, - }, - where: { - id: dbRoles[1].id, - }, - }); - }); - }); -}); + describe('listRoles', () => { + it('should stringify bigint', async () => { + const partialRole: Partial = { + permissions: 4n, + } + + prisma.adminRole.findMany.mockResolvedValueOnce([partialRole]) + const response = await listRoles() + expect(response).toEqual([{ permissions: '4' }]) + }) + }) + + describe('createRole', () => { + it('should create role with incremented position when position 0 is the highest', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 0, + } + + prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole) + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.adminRole.create.mockResolvedValue(null) + await createRole({ name: 'test' }) + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 1 } }) + }) + + it('should create role with incremented position with bigger position', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + } + + prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole) + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.adminRole.create.mockResolvedValue(null) + await createRole({ name: 'test' }) + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 51 } }) + }) + + it('should create role with incremented position with no role in db', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + } + + prisma.adminRole.findFirst.mockResolvedValueOnce(undefined) + prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.adminRole.create.mockResolvedValue(null) + await createRole({ name: 'test' }) + + expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 0 } }) + }) + }) + describe('deleteRole', () => { + const roleId = faker.string.uuid() + it('should delete role and remove id from concerned users', async () => { + const users = [{ + adminRoleIds: [roleId], + id: faker.string.uuid(), + }, { + adminRoleIds: [roleId, faker.string.uuid()], + id: faker.string.uuid(), + }] as const satisfies Partial[] + + prisma.user.findMany.mockResolvedValueOnce(users) + prisma.adminRole.findMany.mockResolvedValueOnce([]) + prisma.adminRole.create.mockResolvedValue(null) + await deleteRole(roleId) + + expect(prisma.user.update).toHaveBeenNthCalledWith(1, { where: { id: users[0].id }, data: { adminRoleIds: [] } }) + expect(prisma.user.update).toHaveBeenNthCalledWith(2, { where: { id: users[1].id }, data: { adminRoleIds: [users[1].adminRoleIds[1]] } }) + expect(prisma.adminRole.delete).toHaveBeenCalledWith({ where: { id: roleId } }) + }) + }) + describe('countRolesMembers', () => { + it('should return aggregated role member counts', async () => { + const partialRoles = [{ + id: faker.string.uuid(), + }, { + id: faker.string.uuid(), + }] as const satisfies Partial[] + + const users = [{ + adminRoleIds: [partialRoles[0].id, partialRoles[1].id], + }, { + adminRoleIds: [partialRoles[1].id], + }] as const satisfies Partial[] + prisma.adminRole.findMany.mockResolvedValue(partialRoles) + prisma.user.findMany.mockResolvedValue(users) + + const response = await countRolesMembers() + + expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 }) + }) + }) + describe('patchRoles', () => { + const dbRoles: AdminRole[] = [{ + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 0, + }, { + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 1, + }] + + it('should do nothing', async () => { + prisma.adminRole.findMany.mockResolvedValue([]) + await patchRoles([]) + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) + }) + + it('should return 400 if incoherent positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 1 }, + ] + prisma.adminRole.findMany.mockResolvedValue(dbRoles) + + const response = await patchRoles(updateRoles) + + expect(response).instanceOf(BadRequest400) + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) + }) + it('should return 400 if incoherent positions (missing roles)', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, position: 1 }, + ] + prisma.adminRole.findMany.mockResolvedValue(dbRoles) + + const response = await patchRoles(updateRoles) + + expect(response).instanceOf(BadRequest400) + expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) + }) + it('should update positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 0 }, + ] + prisma.adminRole.findMany.mockResolvedValue(dbRoles) + + await patchRoles(updateRoles) + + expect(prisma.adminRole.update).toHaveBeenCalledTimes(2) + }) + it('should update permissions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, permissions: '0' }, + ] + prisma.adminRole.findMany.mockResolvedValue(dbRoles) + + await patchRoles(updateRoles) + + expect(prisma.adminRole.update).toHaveBeenCalledTimes(1) + expect(prisma.adminRole.update).toHaveBeenCalledWith({ + data: { + name: dbRoles[1].name, + oidcGroup: dbRoles[1].oidcGroup, + permissions: 0n, + position: 1, + }, + where: { + id: dbRoles[1].id, + }, + }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts index 2198c191c..b9af2b745 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts @@ -1,121 +1,90 @@ -import type { AdminRole, adminRoleContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; -import { listAdminRoles } from '@old-server/resources/queries-index'; -import type { ErrorResType } from '@old-server/utils/errors'; -import { BadRequest400 } from '@old-server/utils/errors'; -import type { Project, ProjectRole } from '@prisma/client'; +import type { Project, ProjectRole } from '@prisma/client' +import type { AdminRole, adminRoleContract } from '@cpn-console/shared' +import { + listAdminRoles, +} from '@old-server/resources/queries-index' +import type { ErrorResType } from '@old-server/utils/errors' +import { BadRequest400 } from '@old-server/utils/errors' +import prisma from '@old-server/prisma' export async function listRoles() { - return listAdminRoles().then((roles) => - roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - ); + return listAdminRoles() + .then(roles => roles.map(role => ({ ...role, permissions: role.permissions.toString() }))) } -export async function patchRoles( - roles: typeof adminRoleContract.patchAdminRoles.body._type, -): Promise { - const dbRoles = await prisma.adminRole.findMany(); - const positionsAvailable: number[] = []; +export async function patchRoles(roles: typeof adminRoleContract.patchAdminRoles.body._type): Promise { + const dbRoles = await prisma.adminRole.findMany() + const positionsAvailable: number[] = [] - const updatedRoles: (Omit & { - permissions: bigint; - })[] = dbRoles - .filter((dbRole) => roles.find((role) => role.id === dbRole.id)) // filter non concerned dbRoles - .map((dbRole) => { - const matchingRole = roles.find((role) => role.id === dbRole.id); - if ( - typeof matchingRole?.position !== 'undefined' && - !positionsAvailable.includes(matchingRole.position) - ) { - positionsAvailable.push(matchingRole.position); - } - return { - id: dbRole.id, - name: matchingRole?.name ?? dbRole.name, - permissions: matchingRole?.permissions - ? BigInt(matchingRole?.permissions) - : dbRole.permissions, - position: matchingRole?.position ?? dbRole.position, - oidcGroup: matchingRole?.oidcGroup ?? dbRole.oidcGroup, - }; - }); + const updatedRoles: (Omit & { permissions: bigint })[] = dbRoles + .filter(dbRole => roles.find(role => role.id === dbRole.id)) // filter non concerned dbRoles + .map((dbRole) => { + const matchingRole = roles.find(role => role.id === dbRole.id) + if (typeof matchingRole?.position !== 'undefined' && !positionsAvailable.includes(matchingRole.position)) { + positionsAvailable.push(matchingRole.position) + } + return { + id: dbRole.id, + name: matchingRole?.name ?? dbRole.name, + permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : dbRole.permissions, + position: matchingRole?.position ?? dbRole.position, + oidcGroup: matchingRole?.oidcGroup ?? dbRole.oidcGroup, + } + }) - if ( - positionsAvailable.length && - positionsAvailable.length !== dbRoles.length - ) - return new BadRequest400( - 'Les numéros de position des rôles sont incohérentes', - ); - for (const { id, ...role } of updatedRoles) { - await prisma.adminRole.update({ where: { id }, data: role }); - } + if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes') + for (const { id, ...role } of updatedRoles) { + await prisma.adminRole.update({ where: { id }, data: role }) + } - return listRoles(); + return listRoles() } -export async function createRole( - role: typeof adminRoleContract.createAdminRole.body._type, -) { - const dbMaxPosRole = - ( - await prisma.adminRole.findFirst({ - orderBy: { position: 'desc' }, - select: { position: true }, - }) - )?.position ?? -1; +export async function createRole(role: typeof adminRoleContract.createAdminRole.body._type) { + const dbMaxPosRole = (await prisma.adminRole.findFirst({ + orderBy: { position: 'desc' }, + select: { position: true }, + }))?.position ?? -1 - await prisma.adminRole.create({ - data: { - ...role, - position: dbMaxPosRole + 1, - permissions: 0n, - }, - }); + await prisma.adminRole.create({ + data: { + ...role, + position: dbMaxPosRole + 1, + permissions: 0n, + }, + }) - return listRoles(); + return listRoles() } export async function countRolesMembers() { - const roles = await prisma.adminRole.findMany({ - where: { oidcGroup: { equals: '' } }, - select: { id: true }, - }); - const roleIds = roles.map((role) => role.id); - const users = await prisma.user.findMany({ - where: { adminRoleIds: { hasSome: roleIds } }, - select: { adminRoleIds: true }, - }); - const rolesCounts: Record = Object.fromEntries( - roles.map((role) => [role.id, 0]), - ); // {role uuid: 0} - for (const { adminRoleIds } of users) { - for (const roleId of adminRoleIds) { - rolesCounts[roleId]++; - } + const roles = await prisma.adminRole.findMany({ where: { oidcGroup: { equals: '' } }, select: { id: true } }) + const roleIds = roles.map(role => role.id) + const users = await prisma.user.findMany({ + where: { adminRoleIds: { hasSome: roleIds } }, + select: { adminRoleIds: true }, + }) + const rolesCounts: Record = Object.fromEntries(roles.map(role => [role.id, 0])) // {role uuid: 0} + for (const { adminRoleIds } of users) { + for (const roleId of adminRoleIds) { + rolesCounts[roleId]++ } - return rolesCounts; + } + return rolesCounts } export async function deleteRole(roleId: Project['id']) { - const allUsers = await prisma.user.findMany({ - where: { - adminRoleIds: { has: roleId }, - }, - }); - for (const user of allUsers) { - await prisma.user.update({ - where: { id: user.id }, - data: { - adminRoleIds: user.adminRoleIds.filter( - (adminRoleId) => adminRoleId !== roleId, - ), - }, - }); - } - await prisma.adminRole.delete({ where: { id: roleId } }); - return null; + const allUsers = await prisma.user.findMany({ + where: { + adminRoleIds: { has: roleId }, + }, + }) + for (const user of allUsers) { + await prisma.user.update({ + where: { id: user.id }, + data: { adminRoleIds: user.adminRoleIds.filter(adminRoleId => adminRoleId !== roleId) }, + }) + } + await prisma.adminRole.delete({ where: { id: roleId } }) + return null } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts index e1d5a9aec..3e35f78df 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts @@ -1,38 +1,32 @@ -import prisma from '@old-server/prisma'; -import type { AdminRole, Prisma } from '@prisma/client'; +import type { + AdminRole, + Prisma, +} from '@prisma/client' +import prisma from '@old-server/prisma' -export const listAdminRoles = () => - prisma.adminRole.findMany({ orderBy: { position: 'asc' } }); +export const listAdminRoles = () => prisma.adminRole.findMany({ orderBy: { position: 'asc' } }) -export function createAdminRole( - data: Pick, -) { - return prisma.adminRole.create({ - data: { - name: data.name, - permissions: 0n, - position: data.position, - }, - }); +export function createAdminRole(data: Pick) { + return prisma.adminRole.create({ + data: { + name: data.name, + permissions: 0n, + position: data.position, + }, + }) } -export function updateAdminRole( - id: AdminRole['id'], - data: Pick< - Prisma.AdminRoleUncheckedUpdateInput, - 'permissions' | 'name' | 'position' | 'id' - >, -) { - return prisma.projectRole.updateMany({ - where: { id }, - data, - }); +export function updateAdminRole(id: AdminRole['id'], data: Pick) { + return prisma.projectRole.updateMany({ + where: { id }, + data, + }) } export function deleteAdminRole(id: AdminRole['id']) { - return prisma.projectRole.delete({ - where: { - id, - }, - }); + return prisma.projectRole.delete({ + where: { + id, + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts index 60e5173aa..e652c8262 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts @@ -1,223 +1,181 @@ -import { adminRoleContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessListRolesMock = vi.spyOn(business, 'listRoles'); -const businessCreateRoleMock = vi.spyOn(business, 'createRole'); -const businessPatchRolesMock = vi.spyOn(business, 'patchRoles'); -const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers'); -const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole'); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { adminRoleContract } from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { BadRequest400 } from '../../utils/errors' +import { getUserMockInfos } from '../../utils/mocks' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListRolesMock = vi.spyOn(business, 'listRoles') +const businessCreateRoleMock = vi.spyOn(business, 'createRole') +const businessPatchRolesMock = vi.spyOn(business, 'patchRoles') +const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers') +const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole') describe('test adminRoleContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('listAdminRoles', () => { - it('should return list of admin roles', async () => { - const roles = [ - { - id: faker.string.uuid(), - name: 'Role 1', - oidcGroup: '', - position: 0, - permissions: '1', - }, - ]; - businessListRolesMock.mockResolvedValueOnce(roles); - - const response = await app - .inject() - .get(adminRoleContract.listAdminRoles.path) - .end(); - - expect(businessListRolesMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(roles); - expect(response.statusCode).toEqual(200); - }); - }); - - describe('createAdminRole', () => { - it('should create a role for authorized users', async () => { - const user = getUserMockInfos(true); - const newRole = { id: 'newRole', name: 'New Role' }; - const roleData = { name: 'New Role' }; - - authUserMock.mockResolvedValueOnce(user); - businessCreateRoleMock.mockResolvedValueOnce(newRole); - - const response = await app - .inject() - .post(adminRoleContract.createAdminRole.path) - .body(roleData) - .end(); - - expect(businessCreateRoleMock).toHaveBeenCalledWith(roleData); - expect(response.json()).toEqual(newRole); - expect(response.statusCode).toEqual(201); - }); - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(adminRoleContract.createAdminRole.path) - .body({ name: 'New Role' }) - .end(); - - expect(businessCreateRoleMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('patchAdminRoles', () => { - const updatedRoles = [ - { - id: faker.string.uuid(), - name: 'Role 1', - oidcGroup: '', - position: 0, - permissions: '1', - }, - ]; - const rolesData = [{ id: updatedRoles[0].id, name: 'Updated Role' }]; - it('should update roles for authorized users', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessPatchRolesMock.mockResolvedValueOnce(updatedRoles); - - const response = await app - .inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end(); - - expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData); - expect(response.json()).toEqual(updatedRoles); - expect(response.statusCode).toEqual(200); - }); - - it('should return error if business logic fails', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessPatchRolesMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - - const response = await app - .inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end(); - - expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData); - expect(response.statusCode).toEqual(400); - }); - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end(); - - expect(businessPatchRolesMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('adminRoleMemberCounts', () => { - it('should return counts of role members for admin', async () => { - const user = getUserMockInfos(true); - const counts = { role1: 5, role2: 3 }; - - authUserMock.mockResolvedValueOnce(user); - businessCountRolesMembersMock.mockResolvedValueOnce(counts); - - const response = await app - .inject() - .get(adminRoleContract.adminRoleMemberCounts.path) - .end(); - - expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(counts); - expect(response.statusCode).toEqual(200); - }); - - it('should return 403 if user is not admin', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(adminRoleContract.adminRoleMemberCounts.path) - .end(); - - expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('deleteAdminRole', () => { - const roleId = faker.string.uuid(); - it('should delete a role for authorized users', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessDeleteRoleMock.mockResolvedValueOnce(null); - - const response = await app - .inject() - .delete( - adminRoleContract.deleteAdminRole.path.replace( - ':roleId', - roleId, - ), - ) - .end(); - - expect(businessDeleteRoleMock).toHaveBeenCalledWith(roleId); - expect(response.statusCode).toEqual(204); - }); - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - adminRoleContract.deleteAdminRole.path.replace( - ':roleId', - roleId, - ), - ) - .end(); - - expect(businessDeleteRoleMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listAdminRoles', () => { + it('should return list of admin roles', async () => { + const roles = [{ id: faker.string.uuid(), name: 'Role 1', oidcGroup: '', position: 0, permissions: '1' }] + businessListRolesMock.mockResolvedValueOnce(roles) + + const response = await app.inject() + .get(adminRoleContract.listAdminRoles.path) + .end() + + expect(businessListRolesMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(roles) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('createAdminRole', () => { + it('should create a role for authorized users', async () => { + const user = getUserMockInfos(true) + const newRole = { id: 'newRole', name: 'New Role' } + const roleData = { name: 'New Role' } + + authUserMock.mockResolvedValueOnce(user) + businessCreateRoleMock.mockResolvedValueOnce(newRole) + + const response = await app.inject() + .post(adminRoleContract.createAdminRole.path) + .body(roleData) + .end() + + expect(businessCreateRoleMock).toHaveBeenCalledWith(roleData) + expect(response.json()).toEqual(newRole) + expect(response.statusCode).toEqual(201) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(adminRoleContract.createAdminRole.path) + .body({ name: 'New Role' }) + .end() + + expect(businessCreateRoleMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('patchAdminRoles', () => { + const updatedRoles = [{ id: faker.string.uuid(), name: 'Role 1', oidcGroup: '', position: 0, permissions: '1' }] + const rolesData = [{ id: updatedRoles[0].id, name: 'Updated Role' }] + it('should update roles for authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessPatchRolesMock.mockResolvedValueOnce(updatedRoles) + + const response = await app.inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end() + + expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData) + expect(response.json()).toEqual(updatedRoles) + expect(response.statusCode).toEqual(200) + }) + + it('should return error if business logic fails', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessPatchRolesMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + + const response = await app.inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end() + + expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData) + expect(response.statusCode).toEqual(400) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(adminRoleContract.patchAdminRoles.path) + .body(rolesData) + .end() + + expect(businessPatchRolesMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('adminRoleMemberCounts', () => { + it('should return counts of role members for admin', async () => { + const user = getUserMockInfos(true) + const counts = { role1: 5, role2: 3 } + + authUserMock.mockResolvedValueOnce(user) + businessCountRolesMembersMock.mockResolvedValueOnce(counts) + + const response = await app.inject() + .get(adminRoleContract.adminRoleMemberCounts.path) + .end() + + expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(counts) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 if user is not admin', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(adminRoleContract.adminRoleMemberCounts.path) + .end() + + expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('deleteAdminRole', () => { + const roleId = faker.string.uuid() + it('should delete a role for authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessDeleteRoleMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .delete(adminRoleContract.deleteAdminRole.path.replace(':roleId', roleId)) + .end() + + expect(businessDeleteRoleMock).toHaveBeenCalledWith(roleId) + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(adminRoleContract.deleteAdminRole.path.replace(':roleId', roleId)) + .end() + + expect(businessDeleteRoleMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts index 9a7601d05..268ffb696 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts @@ -1,85 +1,74 @@ -import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - +import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared' import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from './business'; - -@Injectable() -export class AdminRoleRouterService { - constructor(private readonly appService: AppService) {} - - adminRoleRouter() { - return this.appService.serverInstance.router(adminRoleContract, { - // Récupérer des projets - listAdminRoles: async () => { - const body = await listRoles(); - - return { - status: 200, - body, - }; - }, - - createAdminRole: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await createRole(body); - - return { - status: 201, - body: resBody, - }; - }, - - patchAdminRoles: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await patchRoles(body); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - adminRoleMemberCounts: async ({ request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await countRolesMembers(); - - return { - status: 200, - body: resBody, - }; - }, - - deleteAdminRole: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await deleteRole(params.roleId); - - return { - status: 204, - body: resBody, - }; - }, - }); - } + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' + +export function adminRoleRouter() { + return serverInstance.router(adminRoleContract, { + // Récupérer des projets + listAdminRoles: async () => { + const body = await listRoles() + + return { + status: 200, + body, + } + }, + + createAdminRole: async ({ request: req, body }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await createRole(body) + + return { + status: 201, + body: resBody, + } + }, + + patchAdminRoles: async ({ request: req, body }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await patchRoles(body) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 200, + body: resBody, + } + }, + + adminRoleMemberCounts: async ({ request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await countRolesMembers() + + return { + status: 200, + body: resBody, + } + }, + + deleteAdminRole: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const resBody = await deleteRole(params.roleId) + + return { + status: 204, + body: resBody, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts index a60a38eae..83fa13fb4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts @@ -1,82 +1,73 @@ -import type { AdminToken } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { describe, expect, it } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import { createToken, deleteToken, listTokens } from './business'; +import { describe, expect, it } from 'vitest' +import type { AdminToken } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import prisma from '../../__mocks__/prisma' +import { createToken, deleteToken, listTokens } from './business' describe('test admin-token business', () => { - describe('listTokens', () => { - it('should stringify bigint', async () => { - const partialtoken: Partial = { - permissions: 4n, - }; + describe('listTokens', () => { + it('should stringify bigint', async () => { + const partialtoken: Partial = { + permissions: 4n, + } - prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]); - const response = await listTokens({}); - expect(response).toEqual([{ permissions: '4' }]); - }); - it('should return revoked', async () => { - const partialtoken: Partial = { - permissions: 4n, - status: 'revoked', - }; + prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]) + const response = await listTokens({}) + expect(response).toEqual([{ permissions: '4' }]) + }) + it('should return revoked', async () => { + const partialtoken: Partial = { + permissions: 4n, + status: 'revoked', + } - prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]); - const response = await listTokens({ withRevoked: true }); - expect(response).toEqual([{ ...partialtoken, permissions: '4' }]); - }); - }); + prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]) + const response = await listTokens({ withRevoked: true }) + expect(response).toEqual([{ ...partialtoken, permissions: '4' }]) + }) + }) - describe('createToken', () => { - it('should create ', async () => { - const dbToken: Partial = undefined; - const userId = faker.string.uuid(); - const createdToken: AdminToken = { - expirationDate: null, - id: faker.string.uuid(), - name: 'test', - permissions: '2', - }; - prisma.adminToken.findUnique.mockResolvedValueOnce(dbToken); - prisma.adminToken.create.mockResolvedValueOnce(createdToken); - await createToken( - { name: 'test', permissions: '2', expirationDate: null }, - userId, - undefined, - ); + describe('createToken', () => { + it('should create ', async () => { + const dbToken: Partial = undefined + const userId = faker.string.uuid() + const createdToken: AdminToken = { + expirationDate: null, + id: faker.string.uuid(), + name: 'test', + permissions: '2', + } + prisma.adminToken.findUnique.mockResolvedValueOnce(dbToken) + prisma.adminToken.create.mockResolvedValueOnce(createdToken) + await createToken({ name: 'test', permissions: '2', expirationDate: null }, userId, undefined) - expect(prisma.adminToken.create).toHaveBeenCalledWith({ - data: { - name: 'test', - hash: expect.any(String), - permissions: 2n, - userId: expect.any(String), - expirationDate: undefined, - }, - omit: expect.any(Object), - include: { - owner: true, - }, - }); - }); - it('should not create cause expiration is too short', async () => { - const expirationDate = new Date(); - await createToken({ - name: 'test', - permissions: '2', - expirationDate: expirationDate.toISOString(), - }); + expect(prisma.adminToken.create).toHaveBeenCalledWith({ + data: { + name: 'test', + hash: expect.any(String), + permissions: 2n, + userId: expect.any(String), + expirationDate: undefined, + }, + omit: expect.any(Object), + include: { + owner: true, + }, + }) + }) + it('should not create cause expiration is too short', async () => { + const expirationDate = new Date() + await createToken({ name: 'test', permissions: '2', expirationDate: expirationDate.toISOString() }) - expect(prisma.adminToken.create).toHaveBeenCalledTimes(0); - }); - }); + expect(prisma.adminToken.create).toHaveBeenCalledTimes(0) + }) + }) - describe('deleteToken', () => { - it('should delete token', async () => { - prisma.adminToken.delete.mockResolvedValueOnce(undefined); - await deleteToken(faker.string.uuid()); - expect(prisma.adminToken.updateMany).toHaveBeenCalledTimes(1); - }); - }); -}); + describe('deleteToken', () => { + it('should delete token', async () => { + prisma.adminToken.delete.mockResolvedValueOnce(undefined) + await deleteToken(faker.string.uuid()) + expect(prisma.adminToken.updateMany).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts index 6705eb6c3..c5307fdca 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts @@ -1,90 +1,68 @@ -import { - type adminTokenContract, - generateRandomPassword, - isAtLeastTomorrow, -} from '@cpn-console/shared'; -import { BadRequest400 } from '@old-server/utils/errors'; -import type { $Enums, AdminToken, Prisma } from '@prisma/client'; -import { createHash, randomUUID } from 'node:crypto'; +import { createHash, randomUUID } from 'node:crypto' +import { type adminTokenContract, generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' +import type { $Enums, AdminToken, Prisma } from '@prisma/client' +import prisma from '../../prisma' +import { BadRequest400 } from '@old-server/utils/errors' -import prisma from '../../prisma'; +export async function listTokens(query: typeof adminTokenContract.listAdminTokens.query._type) { + const where = { + status: { + in: ['active'] as $Enums.TokenStatus[], + }, + } as const satisfies Prisma.AdminTokenWhereInput -export async function listTokens( - query: typeof adminTokenContract.listAdminTokens.query._type, -) { - const where = { - status: { - in: ['active'] as $Enums.TokenStatus[], - }, - } as const satisfies Prisma.AdminTokenWhereInput; + if (query?.withRevoked) where.status.in.push('revoked') - if (query?.withRevoked) where.status.in.push('revoked'); - - return prisma.adminToken - .findMany({ - omit: { hash: true }, - include: { owner: true }, - orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], - where, - }) - .then((tokens) => - tokens.map(({ permissions, ...token }) => ({ - permissions: permissions.toString(), - ...token, - })), - ); + return prisma.adminToken.findMany({ + omit: { hash: true }, + include: { owner: true }, + orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], + where, + }).then(tokens => + tokens.map(({ permissions, ...token }) => ({ permissions: permissions.toString(), ...token })), + ) } -export async function createToken( - data: typeof adminTokenContract.createAdminToken.body._type, -) { - if ( - data.expirationDate && - !isAtLeastTomorrow(new Date(data.expirationDate)) - ) { - return new BadRequest400("Date d'expiration trop courte"); - } - const password = generateRandomPassword( - 48, - 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-', - ); - const hash = createHash('sha256').update(password).digest('hex'); - const botUserId = randomUUID(); - await prisma.user.create({ - data: { - firstName: 'Bot Admin', - lastName: data.name, - type: 'bot', - id: botUserId, - email: `${botUserId}@bot.io`, - }, - }); - const token = await prisma.adminToken.create({ - data: { - ...data, - hash, - permissions: BigInt(data.permissions), - expirationDate: data.expirationDate - ? new Date(data.expirationDate) - : undefined, - userId: botUserId, - }, - omit: { hash: true }, - include: { owner: true }, - }); - return { - ...token, - password, - permissions: token.permissions.toString(), - }; +export async function createToken(data: typeof adminTokenContract.createAdminToken.body._type) { + if (data.expirationDate && !isAtLeastTomorrow(new Date(data.expirationDate))) { + return new BadRequest400('Date d\'expiration trop courte') + } + const password = generateRandomPassword(48, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') + const hash = createHash('sha256').update(password).digest('hex') + const botUserId = randomUUID() + await prisma.user.create({ + data: { + firstName: 'Bot Admin', + lastName: data.name, + type: 'bot', + id: botUserId, + email: `${botUserId}@bot.io`, + }, + }) + const token = await prisma.adminToken.create({ + data: { + ...data, + hash, + permissions: BigInt(data.permissions), + expirationDate: data.expirationDate ? new Date(data.expirationDate) : undefined, + userId: botUserId, + }, + omit: { hash: true }, + include: { owner: true }, + }) + return { + ...token, + password, + permissions: token.permissions.toString(), + } } export async function deleteToken(id: AdminToken['id']) { - return prisma.adminToken.updateMany({ - where: { id }, - data: { - status: 'revoked', - expirationDate: new Date(Date.now()), - }, - }); + return prisma.adminToken.updateMany({ + where: { id }, + data: { + status: 'revoked', + expirationDate: new Date(Date.now()), + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts index f137d1156..92f371452 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts @@ -1,186 +1,161 @@ -import type { ExposedAdminToken } from '@cpn-console/shared'; -import { adminTokenContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import type { AdminToken } from '@prisma/client'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessListTokensMock = vi.spyOn(business, 'listTokens'); -const businessCreateTokenMock = vi.spyOn(business, 'createToken'); -const businessDeleteTokenMock = vi.spyOn(business, 'deleteToken'); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ExposedAdminToken } from '@cpn-console/shared' +import { adminTokenContract } from '@cpn-console/shared' +import type { AdminToken } from '@prisma/client' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getUserMockInfos } from '../../utils/mocks' +import { BadRequest400 } from '../../utils/errors' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListTokensMock = vi.spyOn(business, 'listTokens') +const businessCreateTokenMock = vi.spyOn(business, 'createToken') +const businessDeleteTokenMock = vi.spyOn(business, 'deleteToken') describe('test adminTokenContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('listAdminTokens', () => { - it('should return list of admin tokens', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - const tokens: AdminToken[] = [ - { - id: faker.string.uuid(), - name: 'token1', - permissions: '2', - lastUse: new Date().toISOString(), - expirationDate: null, - status: 'active', - createdAt: new Date(Date.now()).toISOString(), - }, - ]; - businessListTokensMock.mockResolvedValueOnce(tokens); - - const response = await app - .inject() - .get(adminTokenContract.listAdminTokens.path) - .end(); - - expect(businessListTokensMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(tokens); - expect(response.statusCode).toEqual(200); - }); - - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(adminTokenContract.listAdminTokens.path) - .end(); - - expect(businessListTokensMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('createAdminToken', () => { - it('should create a token for authorized users', async () => { - const user = getUserMockInfos(true); - - const newToken = { - id: faker.string.uuid(), - name: 'test', - lastUse: null, - expirationDate: null, - password: faker.string.alpha({ casing: 'lower', length: 10 }), - permissions: '2', - createdAt: new Date(Date.now()).toISOString(), - status: 'active', - }; - const tokenData: ExposedAdminToken = { - name: newToken.name, - permissions: newToken.permissions, - expirationDate: null, - }; - - authUserMock.mockResolvedValueOnce(user); - businessCreateTokenMock.mockResolvedValueOnce(newToken); - - const response = await app - .inject() - .post(adminTokenContract.createAdminToken.path) - .body(tokenData) - .end(); - - expect(businessCreateTokenMock).toHaveBeenCalledWith(tokenData); - expect(response.json()).toEqual(newToken); - expect(response.statusCode).toEqual(201); - }); - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(adminTokenContract.createAdminToken.path) - .body({ - name: 'new-token', - expirationDate: null, - permissions: '4', - }) - .end(); - - expect(businessCreateTokenMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - - it('should pass business error', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessCreateTokenMock.mockResolvedValueOnce( - new BadRequest400('Invalid date'), - ); - - const response = await app - .inject() - .post(adminTokenContract.createAdminToken.path) - .body({ - name: 'new-token', - expirationDate: null, - permissions: '4', - }) - .end(); - - expect(businessCreateTokenMock).toHaveBeenCalledTimes(1); - expect(response.statusCode).toEqual(400); - }); - }); - - describe('deleteAdminToken', () => { - const tokenId = faker.string.uuid(); - it('should delete a token for authorized users', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessDeleteTokenMock.mockResolvedValueOnce(null); - - const response = await app - .inject() - .delete( - adminTokenContract.deleteAdminToken.path.replace( - ':tokenId', - tokenId, - ), - ) - .end(); - - expect(businessDeleteTokenMock).toHaveBeenCalledWith(tokenId); - expect(response.statusCode).toEqual(204); - }); - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - adminTokenContract.deleteAdminToken.path.replace( - ':tokenId', - tokenId, - ), - ) - .end(); - - expect(businessDeleteTokenMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listAdminTokens', () => { + it('should return list of admin tokens', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + const tokens: AdminToken[] = [{ + id: faker.string.uuid(), + name: 'token1', + permissions: '2', + lastUse: (new Date()).toISOString(), + expirationDate: null, + status: 'active', + createdAt: (new Date(Date.now())).toISOString(), + }] + businessListTokensMock.mockResolvedValueOnce(tokens) + + const response = await app.inject() + .get(adminTokenContract.listAdminTokens.path) + .end() + + expect(businessListTokensMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(tokens) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(adminTokenContract.listAdminTokens.path) + .end() + + expect(businessListTokensMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('createAdminToken', () => { + it('should create a token for authorized users', async () => { + const user = getUserMockInfos(true) + + const newToken = { + id: faker.string.uuid(), + name: 'test', + lastUse: null, + expirationDate: null, + password: faker.string.alpha({ casing: 'lower', length: 10 }), + permissions: '2', + createdAt: (new Date(Date.now())).toISOString(), + status: 'active', + } + const tokenData: ExposedAdminToken = { + name: newToken.name, + permissions: newToken.permissions, + expirationDate: null, + } + + authUserMock.mockResolvedValueOnce(user) + businessCreateTokenMock.mockResolvedValueOnce(newToken) + + const response = await app.inject() + .post(adminTokenContract.createAdminToken.path) + .body(tokenData) + .end() + + expect(businessCreateTokenMock).toHaveBeenCalledWith(tokenData) + expect(response.json()).toEqual(newToken) + expect(response.statusCode).toEqual(201) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(adminTokenContract.createAdminToken.path) + .body({ + name: 'new-token', + expirationDate: null, + permissions: '4', + }) + .end() + + expect(businessCreateTokenMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + + it('should pass business error', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessCreateTokenMock.mockResolvedValueOnce(new BadRequest400('Invalid date')) + + const response = await app.inject() + .post(adminTokenContract.createAdminToken.path) + .body({ + name: 'new-token', + expirationDate: null, + permissions: '4', + }) + .end() + + expect(businessCreateTokenMock).toHaveBeenCalledTimes(1) + expect(response.statusCode).toEqual(400) + }) + }) + + describe('deleteAdminToken', () => { + const tokenId = faker.string.uuid() + it('should delete a token for authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessDeleteTokenMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .delete(adminTokenContract.deleteAdminToken.path.replace(':tokenId', tokenId)) + .end() + + expect(businessDeleteTokenMock).toHaveBeenCalledWith(tokenId) + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(adminTokenContract.deleteAdminToken.path.replace(':tokenId', tokenId)) + .end() + + expect(businessDeleteTokenMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts index de84c8b34..301b465d7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts @@ -1,54 +1,44 @@ -import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - -import { AppService } from '../../app'; -import { createToken, deleteToken, listTokens } from './business'; - -@Injectable() -export class AdminTokenRouterService { - constructor(private readonly appService: AppService) {} - - adminTokenRouter() { - return this.appService.serverInstance.router(adminTokenContract, { - listAdminTokens: async ({ request: req, query }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await listTokens(query); - - return { - status: 200, - body, - }; - }, - - createAdminToken: async ({ request: req, body: data }) => { - const perms = await authUser(req); - - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await createToken(data); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - deleteAdminToken: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - await deleteToken(params.tokenId); - - return { - status: 204, - body: null, - }; - }, - }); - } +import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared' +import { serverInstance } from '../../app' +import { createToken, deleteToken, listTokens } from './business' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' + +export function adminTokenRouter() { + return serverInstance.router(adminTokenContract, { + listAdminTokens: async ({ request: req, query }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + const body = await listTokens(query) + + return { + status: 200, + body, + } + }, + + createAdminToken: async ({ request: req, body: data }) => { + const perms = await authUser(req) + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + const body = await createToken(data) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + deleteAdminToken: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + await deleteToken(params.tokenId) + + return { + status: 204, + body: null, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts index 44e8a4ab3..2280f9de8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts @@ -1,293 +1,173 @@ -import { faker } from '@faker-js/faker'; -import type { Cluster, Environment } from '@prisma/client'; -import { describe, expect, it, vi } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import { hook } from '../../__mocks__/utils/hook-wrapper'; -import { - BadRequest400, - ErrorResType, - NotFound404, - Unprocessable422, -} from '../../utils/errors'; -import { - createCluster, - deleteCluster, - getClusterAssociatedEnvironments, - getClusterDetails, - getClusterUsage, - listClusters, - updateCluster, -} from './business'; +import { describe, expect, it, vi } from 'vitest' +import { faker } from '@faker-js/faker' +import type { Cluster, Environment } from '@prisma/client' +import prisma from '../../__mocks__/prisma' +import { hook } from '../../__mocks__/utils/hook-wrapper' +import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '../../utils/errors' +import { createCluster, deleteCluster, getClusterAssociatedEnvironments, getClusterDetails, getClusterUsage, listClusters, updateCluster } from './business' vi.mock('../../utils/hook-wrapper', async () => ({ - hook, -})); + hook, +})) -const userId = faker.string.uuid(); -const requestId = faker.string.uuid(); +const userId = faker.string.uuid() +const requestId = faker.string.uuid() const cluster: Cluster = { - id: faker.string.uuid(), - infos: faker.lorem.lines(2), - privacy: 'public', - createdAt: new Date(), - updatedAt: new Date(), - zoneId: faker.string.uuid(), - clusterResources: false, - kubeConfigId: faker.string.uuid(), - label: faker.string.alpha(10), - secretName: faker.string.alpha(10), - external: false, - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), -}; + id: faker.string.uuid(), + infos: faker.lorem.lines(2), + privacy: 'public', + createdAt: new Date(), + updatedAt: new Date(), + zoneId: faker.string.uuid(), + clusterResources: false, + kubeConfigId: faker.string.uuid(), + label: faker.string.alpha(10), + secretName: faker.string.alpha(10), + external: false, + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), +} describe('test Cluster business logic', () => { - describe('listClusters', () => { - it('should filter for user', async () => { - prisma.cluster.findMany.mockResolvedValue([]); - await listClusters(userId); - expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1); - expect(prisma.cluster.findMany).toHaveBeenCalledWith({ - select: expect.any(Object), - where: { - OR: [ - { privacy: 'public' }, - expect.any(Object), - expect.any(Object), - expect.any(Object), - ], - }, - }); - }); - it('should not filter', async () => { - const dbStages = [{ id: faker.string.uuid() }]; - prisma.cluster.findMany.mockResolvedValue([ - { stages: dbStages }, - ] as unknown as Cluster[]); - const response = await listClusters(); - expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1); - expect(prisma.cluster.findMany).toHaveBeenCalledWith({ - select: expect.any(Object), - where: {}, - }); - expect(response[0].stageIds).toStrictEqual([dbStages[0].id]); - }); - }); - - describe('getClusterAssociatedEnvironments', () => { - it('should list all environments attached to a cluster', async () => { - const envName = faker.string.alpha(8); - const projectName = faker.string.alpha(8); - const ownerEmail = faker.internet.email(); - const cpu = faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }); - const gpu = faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }); - const memory = faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }); - const envs = [ - { - name: envName, - cpu, - gpu, - memory, - project: { - name: projectName, - owner: { email: ownerEmail }, - }, - }, - ] as unknown as Environment[]; - prisma.environment.findMany.mockResolvedValue(envs); - const response = await getClusterAssociatedEnvironments(cluster.id); - expect(response).toStrictEqual([ - { - name: envName, - project: projectName, - owner: ownerEmail, - cpu, - gpu, - memory, - }, - ]); - }); - }); - - describe('getClusterDetails', () => { - it('should return a cluster details', async () => { - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ - ...cluster, - projects: [], - stages: [], - kubeconfig: { user: {}, cluster: {} }, - } as Cluster); - await getClusterDetails(cluster.id); - }); - it('should return a cluster details, without infos in db', async () => { - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ - ...cluster, - infos: null, - projects: [], - stages: [], - kubeconfig: { user: {}, cluster: {} }, - } as Cluster); - const response = await getClusterDetails(cluster.id); - expect(response.infos).toBe(''); - }); - }); - - describe('getClusterUsage', () => { - it('should return a cluster usage', async () => { - prisma.environment.aggregate.mockResolvedValue({ - _count: {}, - _avg: {}, - _min: {}, - _max: {}, - _sum: { - cpu: 10, - gpu: 5, - memory: 20, - }, - }); - const response = await getClusterUsage(cluster.id); - expect(response).toStrictEqual({ - cpu: 10, - gpu: 5, - memory: 20, - }); - }); - }); - - describe('createCluster', () => { - it('should create cluster', async () => { - hook.cluster.upsert.mockResolvedValue({ failed: false }); - prisma.cluster.findUnique.mockResolvedValue(null); - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ - ...cluster, - projects: [], - stages: [], - kubeconfig: { user: {}, cluster: {} }, - } as Cluster); - prisma.cluster.create.mockResolvedValue(cluster); - - const response = await createCluster( - { - infos: faker.string.alpha(10), - zoneId: faker.string.uuid(), - privacy: 'public', - stageIds: [], - clusterResources: false, - kubeconfig: { - cluster: { tlsServerName: faker.internet.domainName() }, - user: {}, - }, - label: faker.string.alpha(10), - external: false, - cpu: faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }), - gpu: faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }), - memory: faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }), - }, - userId, - requestId, - ); - - expect(response).not.instanceOf(ErrorResType); - expect(prisma.cluster.create).toHaveBeenCalled(); - }); - }); - - describe('updateCluster', () => { - it('should update cluster', async () => { - hook.cluster.upsert.mockResolvedValue({ failed: false }); - prisma.cluster.findUnique.mockResolvedValue(cluster); - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ - ...cluster, - projects: [], - stages: [], - kubeconfig: { user: {}, cluster: {} }, - } as Cluster); - prisma.cluster.update.mockResolvedValue(cluster); - - const response = await updateCluster( - { - infos: faker.string.alpha(10), - zoneId: faker.string.uuid(), - privacy: 'public', - stageIds: [], - }, - cluster.id, - userId, - requestId, - ); - - expect(response).not.instanceOf(ErrorResType); - expect(prisma.cluster.update).toHaveBeenCalled(); - }); - it('should return 404', async () => { - prisma.cluster.findUnique.mockResolvedValue(null); - const response = await updateCluster( - { infos: faker.string.alpha(10) }, - cluster.id, - userId, - requestId, - ); - expect(response).instanceOf(NotFound404); - }); - }); - - describe('deleteCluster', () => { - it('should delete cluster', async () => { - hook.cluster.delete.mockResolvedValue({}); - await deleteCluster({ clusterId: cluster.id, userId, requestId }); - - expect(prisma.cluster.delete).toHaveBeenCalledTimes(1); - }); - it('should return failed hook', async () => { - hook.cluster.delete.mockResolvedValue({ failed: true }); - const response = await deleteCluster({ - clusterId: cluster.id, - userId, - requestId, - }); - - expect(response).instanceOf(Unprocessable422); - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0); - }); - it('should not delete cluster, env attached', async () => { - prisma.environment.findFirst.mockResolvedValue({ - id: faker.string.uuid(), - } as Environment); - const response = await deleteCluster({ - clusterId: cluster.id, - userId, - requestId, - }); - - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0); - expect(response).instanceOf(BadRequest400); - }); - }); -}); + describe('listClusters', () => { + it('should filter for user', async () => { + prisma.cluster.findMany.mockResolvedValue([]) + await listClusters(userId) + expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1) + expect(prisma.cluster.findMany).toHaveBeenCalledWith({ select: expect.any(Object), where: { OR: [{ privacy: 'public' }, expect.any(Object), expect.any(Object), expect.any(Object)] } }) + }) + it('should not filter', async () => { + const dbStages = [{ id: faker.string.uuid() }] + prisma.cluster.findMany.mockResolvedValue([{ stages: dbStages }] as unknown as Cluster[]) + const response = await listClusters() + expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1) + expect(prisma.cluster.findMany).toHaveBeenCalledWith({ select: expect.any(Object), where: {} }) + expect(response[0].stageIds).toStrictEqual([dbStages[0].id]) + }) + }) + + describe('getClusterAssociatedEnvironments', () => { + it('should list all environments attached to a cluster', async () => { + const envName = faker.string.alpha(8) + const projectName = faker.string.alpha(8) + const ownerEmail = faker.internet.email() + const cpu = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) + const gpu = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) + const memory = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) + const envs = [{ name: envName, cpu, gpu, memory, project: { name: projectName, owner: { email: ownerEmail } } }] as unknown as Environment[] + prisma.environment.findMany.mockResolvedValue(envs) + const response = await getClusterAssociatedEnvironments(cluster.id) + expect(response).toStrictEqual([{ + name: envName, + project: projectName, + owner: ownerEmail, + cpu, + gpu, + memory, + }]) + }) + }) + + describe('getClusterDetails', () => { + it('should return a cluster details', async () => { + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) + await getClusterDetails(cluster.id) + }) + it('should return a cluster details, without infos in db', async () => { + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, infos: null, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) + const response = await getClusterDetails(cluster.id) + expect(response.infos).toBe('') + }) + }) + + describe('getClusterUsage', () => { + it('should return a cluster usage', async () => { + prisma.environment.aggregate.mockResolvedValue({ _count: {}, _avg: {}, _min: {}, _max: {}, _sum: { + cpu: 10, + gpu: 5, + memory: 20, + } }) + const response = await getClusterUsage(cluster.id) + expect(response).toStrictEqual({ + cpu: 10, + gpu: 5, + memory: 20, + }) + }) + }) + + describe('createCluster', () => { + it('should create cluster', async () => { + hook.cluster.upsert.mockResolvedValue({ failed: false }) + prisma.cluster.findUnique.mockResolvedValue(null) + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) + prisma.cluster.create.mockResolvedValue(cluster) + + const response = await createCluster({ + infos: faker.string.alpha(10), + zoneId: faker.string.uuid(), + privacy: 'public', + stageIds: [], + clusterResources: false, + kubeconfig: { cluster: { tlsServerName: faker.internet.domainName() }, user: {} }, + label: faker.string.alpha(10), + external: false, + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + }, userId, requestId) + + expect(response).not.instanceOf(ErrorResType) + expect(prisma.cluster.create).toHaveBeenCalled() + }) + }) + + describe('updateCluster', () => { + it('should update cluster', async () => { + hook.cluster.upsert.mockResolvedValue({ failed: false }) + prisma.cluster.findUnique.mockResolvedValue(cluster) + prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) + prisma.cluster.update.mockResolvedValue(cluster) + + const response = await updateCluster({ + infos: faker.string.alpha(10), + zoneId: faker.string.uuid(), + privacy: 'public', + stageIds: [], + }, cluster.id, userId, requestId) + + expect(response).not.instanceOf(ErrorResType) + expect(prisma.cluster.update).toHaveBeenCalled() + }) + it('should return 404', async () => { + prisma.cluster.findUnique.mockResolvedValue(null) + const response = await updateCluster({ infos: faker.string.alpha(10) }, cluster.id, userId, requestId) + expect(response).instanceOf(NotFound404) + }) + }) + + describe('deleteCluster', () => { + it('should delete cluster', async () => { + hook.cluster.delete.mockResolvedValue({}) + await deleteCluster({ clusterId: cluster.id, userId, requestId }) + + expect(prisma.cluster.delete).toHaveBeenCalledTimes(1) + }) + it('should return failed hook', async () => { + hook.cluster.delete.mockResolvedValue({ failed: true }) + const response = await deleteCluster({ clusterId: cluster.id, userId, requestId }) + + expect(response).instanceOf(Unprocessable422) + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) + }) + it('should not delete cluster, env attached', async () => { + prisma.environment.findFirst.mockResolvedValue({ id: faker.string.uuid() } as Environment) + const response = await deleteCluster({ clusterId: cluster.id, userId, requestId }) + + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) + expect(response).instanceOf(BadRequest400) + }) + }) +}) // findUniqueOrThrow diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts index 148bb403a..b3e423a07 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts @@ -1,287 +1,230 @@ -import type { - Cluster, - ClusterDetails, - Kubeconfig, - clusterContract, -} from '@cpn-console/shared'; -import { ClusterDetailsSchema, ClusterPrivacy } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; +import type { Prisma, Project, User } from '@prisma/client' +import type { Cluster, ClusterDetails, Kubeconfig, clusterContract } from '@cpn-console/shared' +import { ClusterDetailsSchema, ClusterPrivacy } from '@cpn-console/shared' import { - addLogs, - createCluster as createClusterQuery, - deleteCluster as deleteClusterQuery, - getClusterById, - getClusterByLabel, - getClusterDetails as getClusterDetailsQuery, - getClusterEnvironments, - getProjectsByClusterId, - linkClusterToProjects, - linkZoneToClusters, - listClusters as listClustersQuery, - listStagesByClusterId, - removeClusterFromProject, - removeClusterFromStage, - updateCluster as updateClusterQuery, -} from '@old-server/resources/queries-index'; -import { linkClusterToStages } from '@old-server/resources/stage/business'; -import type { Resources } from '@old-server/types/index'; -import { validateSchema } from '@old-server/utils/business'; -import { - BadRequest400, - ErrorResType, - NotFound404, - Unprocessable422, -} from '@old-server/utils/errors'; -import { hook } from '@old-server/utils/hook-wrapper'; -import type { Prisma, Project, User } from '@prisma/client'; + addLogs, + createCluster as createClusterQuery, + deleteCluster as deleteClusterQuery, + getClusterById, + getClusterByLabel, + getClusterDetails as getClusterDetailsQuery, + getClusterEnvironments, + getProjectsByClusterId, + linkClusterToProjects, + linkZoneToClusters, + listClusters as listClustersQuery, + listStagesByClusterId, + removeClusterFromProject, + removeClusterFromStage, + updateCluster as updateClusterQuery, +} from '@old-server/resources/queries-index' +import { linkClusterToStages } from '@old-server/resources/stage/business' +import { validateSchema } from '@old-server/utils/business' +import { hook } from '@old-server/utils/hook-wrapper' +import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '@old-server/utils/errors' +import prisma from '@old-server/prisma' +import type { Resources } from '@old-server/types/index' export async function listClusters(userId?: User['id']) { - const where: Prisma.ClusterWhereInput = userId - ? { - OR: [ - // Sélectionne tous les clusters publics - { privacy: 'public' }, - // Sélectionne les clusters associés aux projets dont l'user est membre - { - projects: { some: { members: { some: { userId } } } }, - }, - // Sélectionne les clusters associés aux projets dont l'user est owner - { - projects: { some: { ownerId: userId } }, - }, - // Sélectionne les clusters associés aux environnments appartenant à des projets dont l'user est membre - { - environments: { - some: { project: { members: { some: { userId } } } }, - }, - }, - ], - } - : {}; - const clusters = await listClustersQuery(where); - return clusters.map(({ stages, ...cluster }) => ({ - ...cluster, - stageIds: stages.map(({ id }) => id), - })); + const where: Prisma.ClusterWhereInput = userId + ? { + OR: [ + // Sélectionne tous les clusters publics + { privacy: 'public' }, + // Sélectionne les clusters associés aux projets dont l'user est membre + { + projects: { some: { members: { some: { userId } } } }, + }, + // Sélectionne les clusters associés aux projets dont l'user est owner + { + projects: { some: { ownerId: userId } }, + }, + // Sélectionne les clusters associés aux environnments appartenant à des projets dont l'user est membre + { + environments: { some: { project: { members: { some: { userId } } } } }, + }, + ], + } + : {} + const clusters = await listClustersQuery(where) + return clusters.map(({ stages, ...cluster }) => ({ + ...cluster, + stageIds: stages.map(({ id }) => id), + })) } export async function getClusterAssociatedEnvironments(clusterId: string) { - const clusterEnvironments = await getClusterEnvironments(clusterId); - - return clusterEnvironments.map((environment) => { - return { - project: environment.project?.name, - name: environment.name, - owner: environment.project.owner.email, - cpu: environment.cpu, - gpu: environment.gpu, - memory: environment.memory, - }; - }); + const clusterEnvironments = await getClusterEnvironments(clusterId) + + return clusterEnvironments.map((environment) => { + return ({ + project: environment.project?.name, + name: environment.name, + owner: environment.project.owner.email, + cpu: environment.cpu, + gpu: environment.gpu, + memory: environment.memory, + }) + }) } -export async function getClusterDetails( - clusterId: string, -): Promise { - const { infos, projects, stages, kubeconfig, ...details } = - await getClusterDetailsQuery(clusterId); - return { - ...details, - infos: infos ?? '', - projectIds: projects.map((project) => project.id), - stageIds: stages.map(({ id }) => id), - kubeconfig: { - cluster: kubeconfig.cluster as unknown as Kubeconfig['cluster'], - user: kubeconfig.user as unknown as Kubeconfig['user'], - }, - }; +export async function getClusterDetails(clusterId: string): Promise { + const { infos, projects, stages, kubeconfig, ...details } = await getClusterDetailsQuery(clusterId) + return { + ...details, + infos: infos ?? '', + projectIds: projects.map(project => project.id), + stageIds: stages.map(({ id }) => id), + kubeconfig: { + cluster: kubeconfig.cluster as unknown as Kubeconfig['cluster'], + user: kubeconfig.user as unknown as Kubeconfig['user'], + }, + } } export async function getClusterUsage(clusterId: string): Promise { - const clusterUsage = await prisma.environment.aggregate({ - _sum: { - memory: true, - cpu: true, - gpu: true, - }, - where: { - clusterId, - }, - }); - return { - cpu: clusterUsage._sum.cpu ?? 0, - gpu: clusterUsage._sum.gpu ?? 0, - memory: clusterUsage._sum.memory ?? 0, - }; + const clusterUsage = await prisma.environment.aggregate({ + _sum: { + memory: true, + cpu: true, + gpu: true, + }, + where: { + clusterId, + }, + }) + return { + cpu: clusterUsage._sum.cpu ?? 0, + gpu: clusterUsage._sum.gpu ?? 0, + memory: clusterUsage._sum.memory ?? 0, + } } -export async function createCluster( - data: typeof clusterContract.createCluster.body._type, - userId: User['id'], - requestId: string, -) { - const isLabelTaken = await getClusterByLabel(data.label); - if (isLabelTaken) - return new BadRequest400('Ce label existe déjà pour un autre cluster'); +export async function createCluster(data: typeof clusterContract.createCluster.body._type, userId: User['id'], requestId: string) { + const isLabelTaken = await getClusterByLabel(data.label) + if (isLabelTaken) return new BadRequest400('Ce label existe déjà pour un autre cluster') - data.projectIds = - data.privacy === ClusterPrivacy.PUBLIC ? [] : (data.projectIds ?? []); + data.projectIds = data.privacy === ClusterPrivacy.PUBLIC + ? [] + : data.projectIds ?? [] - const { projectIds, stageIds, kubeconfig, zoneId, ...clusterData } = data; + const { + projectIds, + stageIds, + kubeconfig, + zoneId, + ...clusterData + } = data - const clusterCreated = await createClusterQuery( - clusterData, - kubeconfig, - zoneId, - ); + const clusterCreated = await createClusterQuery(clusterData, kubeconfig, zoneId) - if (data.privacy === ClusterPrivacy.DEDICATED && projectIds.length) { - await linkClusterToProjects(clusterCreated.id, projectIds); - } + if (data.privacy === ClusterPrivacy.DEDICATED && projectIds.length) { + await linkClusterToProjects(clusterCreated.id, projectIds) + } - if (stageIds?.length) { - await linkClusterToStages(clusterCreated.id, stageIds); - } + if (stageIds?.length) { + await linkClusterToStages(clusterCreated.id, stageIds) + } - const hookReply = await hook.cluster.upsert(clusterCreated.id, zoneId); - await addLogs({ - action: 'Create Cluster', - data: hookReply, - userId, - requestId, - }); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services à la création du cluster', - ); - } + const hookReply = await hook.cluster.upsert(clusterCreated.id, zoneId) + await addLogs({ action: 'Create Cluster', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la création du cluster') + } - return getClusterDetails(clusterCreated.id); + return getClusterDetails(clusterCreated.id) } -export async function updateCluster( - data: typeof clusterContract.updateCluster.body._type, - clusterId: Cluster['id'], - userId: User['id'], - requestId: string, -): Promise { - if (data?.privacy === ClusterPrivacy.PUBLIC) delete data.projectIds; - - const schemaValidation = ClusterDetailsSchema.partial().safeParse({ - ...data, - id: clusterId, - }); - const validateResult = validateSchema(schemaValidation); - if (validateResult instanceof ErrorResType) return validateResult; - - const dbCluster = await getClusterById(clusterId); - if (!dbCluster) return new NotFound404(); - - const { projectIds, stageIds, kubeconfig, zoneId, ...clusterData } = data; - - const clusterUpdated = await updateClusterQuery( - clusterId, - clusterData, - // @ts-ignore - kubeconfig, - ); - - // zone - if (zoneId) { - await linkZoneToClusters(zoneId, [clusterId]); - } - - // projects - const dbProjects = await getProjectsByClusterId(clusterId); - - let projectsToRemove: Project['id'][] = []; - - if (projectIds && clusterUpdated.privacy === ClusterPrivacy.DEDICATED) { - await linkClusterToProjects(clusterId, projectIds); - projectsToRemove = - dbProjects - ?.map((project) => project.id) - ?.filter((dbProjectId) => !projectIds.includes(dbProjectId)) ?? - []; - } else if (clusterUpdated.privacy === ClusterPrivacy.PUBLIC) { - projectsToRemove = dbProjects?.map((project) => project.id) ?? []; - } - - for (const projectId of projectsToRemove) { - await removeClusterFromProject(clusterUpdated.id, projectId); - } - - // stages - if (stageIds) { - await linkClusterToStages(clusterId, stageIds); - - const dbStages = await listStagesByClusterId(clusterId); - if (dbStages) { - for (const stage of dbStages) { - if (!stageIds.includes(stage.id)) { - await removeClusterFromStage(clusterUpdated.id, stage.id); - } - } +export async function updateCluster(data: typeof clusterContract.updateCluster.body._type, clusterId: Cluster['id'], userId: User['id'], requestId: string): Promise { + if (data?.privacy === ClusterPrivacy.PUBLIC) delete data.projectIds + + const schemaValidation = ClusterDetailsSchema.partial().safeParse({ ...data, id: clusterId }) + const validateResult = validateSchema(schemaValidation) + if (validateResult instanceof ErrorResType) return validateResult + + const dbCluster = await getClusterById(clusterId) + if (!dbCluster) return new NotFound404() + + const { + projectIds, + stageIds, + kubeconfig, + zoneId, + ...clusterData + } = data + + const clusterUpdated = await updateClusterQuery(clusterId, clusterData, + // @ts-ignore + kubeconfig) + + // zone + if (zoneId) { + await linkZoneToClusters(zoneId, [clusterId]) + } + + // projects + const dbProjects = await getProjectsByClusterId(clusterId) + + let projectsToRemove: Project['id'][] = [] + + if (projectIds && clusterUpdated.privacy === ClusterPrivacy.DEDICATED) { + await linkClusterToProjects(clusterId, projectIds) + projectsToRemove = dbProjects?.map(project => project.id)?.filter(dbProjectId => !projectIds.includes(dbProjectId)) ?? [] + } else if (clusterUpdated.privacy === ClusterPrivacy.PUBLIC) { + projectsToRemove = dbProjects?.map(project => project.id) ?? [] + } + + for (const projectId of projectsToRemove) { + await removeClusterFromProject(clusterUpdated.id, projectId) + } + + // stages + if (stageIds) { + await linkClusterToStages(clusterId, stageIds) + + const dbStages = await listStagesByClusterId(clusterId) + if (dbStages) { + for (const stage of dbStages) { + if (!stageIds.includes(stage.id)) { + await removeClusterFromStage(clusterUpdated.id, stage.id) } + } } + } - const hookReply = await hook.cluster.upsert(clusterId, dbCluster.zoneId); - await addLogs({ - action: 'Update Cluster', - data: hookReply, - userId, - requestId, - }); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services à la mise à jour du cluster', - ); - } + const hookReply = await hook.cluster.upsert(clusterId, dbCluster.zoneId) + await addLogs({ action: 'Update Cluster', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la mise à jour du cluster') + } - return getClusterDetails(clusterId); + return getClusterDetails(clusterId) } interface DeleteClusterArgs { - clusterId: Cluster['id']; - userId?: User['id']; - requestId: string; - force?: boolean; + clusterId: Cluster['id'] + userId?: User['id'] + requestId: string + force?: boolean } -export async function deleteCluster({ - clusterId, - requestId, - force, - userId, -}: DeleteClusterArgs) { - let message: string | null = null; - if (force) { - const envs = await prisma.environment.deleteMany({ - where: { clusterId }, - }); - message = `${envs.count} environnements supprimés de force, n'oubliez pas de reprovisionner les projets concernés`; - } else { - const environment = await prisma.environment.findFirst({ - where: { clusterId }, - }); - if (environment) - return new BadRequest400( - 'Impossible de supprimer le cluster, des environnements en activité y sont déployés', - ); - } - - const hookReply = await hook.cluster.delete(clusterId); - await addLogs({ - action: 'Delete Cluster', - data: hookReply, - userId, - requestId, - }); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services à la suppression du cluster', - ); - } - - await deleteClusterQuery(clusterId); - return message; +export async function deleteCluster({ clusterId, requestId, force, userId }: DeleteClusterArgs) { + let message: string | null = null + if (force) { + const envs = await prisma.environment.deleteMany({ + where: { clusterId }, + }) + message = `${envs.count} environnements supprimés de force, n'oubliez pas de reprovisionner les projets concernés` + } else { + const environment = await prisma.environment.findFirst({ where: { clusterId } }) + if (environment) return new BadRequest400('Impossible de supprimer le cluster, des environnements en activité y sont déployés') + } + + const hookReply = await hook.cluster.delete(clusterId) + await addLogs({ action: 'Delete Cluster', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la suppression du cluster') + } + + await deleteClusterQuery(clusterId) + return message } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts index ef645b3ff..fad96194e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts @@ -1,361 +1,312 @@ -import prisma from '@old-server/prisma'; -import type { - Cluster, - Environment, - Kubeconfig, - Prisma, - Project, - Stage, -} from '@prisma/client'; +import type { Cluster, Environment, Kubeconfig, Prisma, Project, Stage } from '@prisma/client' +import prisma from '@old-server/prisma' -export async function getClustersAssociatedWithProject( - projectId: Project['id'], -) { - const [clusterIdsHistory, clusterIdsEnv] = await Promise.all([ - prisma.projectClusterHistory - .findMany({ - select: { - clusterId: true, - }, - where: { - projectId, - }, - }) - .then((history) => history.map(({ clusterId }) => clusterId)), - prisma.cluster - .findMany({ - where: { - environments: { some: { project: { id: projectId } } }, - }, - select: { id: true }, - }) - .then((cluster) => cluster.map(({ id }) => id)), - ]); - const clusterIds = [ - ...clusterIdsHistory, - ...clusterIdsEnv.filter((id) => !clusterIdsHistory.includes(id)), - ]; - return prisma.cluster.findMany({ - where: { id: { in: clusterIds } }, +export async function getClustersAssociatedWithProject(projectId: Project['id']) { + const [ + clusterIdsHistory, + clusterIdsEnv, + ] = await Promise.all([ + prisma.projectClusterHistory.findMany({ + select: { + clusterId: true, + }, + where: { + projectId, + }, + }).then(history => history.map(({ clusterId }) => clusterId)), + prisma.cluster.findMany({ + where: { environments: { some: { project: { id: projectId } } } }, + select: { id: true }, + }).then(cluster => cluster.map(({ id }) => id)), + ]) + const clusterIds = [ + ...clusterIdsHistory, + ...clusterIdsEnv.filter(id => !clusterIdsHistory.includes(id)), + ] + return prisma.cluster.findMany({ + where: { id: { in: clusterIds } }, + select: { + id: true, + infos: true, + label: true, + external: true, + privacy: true, + secretName: true, + kubeconfig: true, + clusterResources: true, + cpu: true, + gpu: true, + memory: true, + zone: { select: { - id: true, - infos: true, - label: true, - external: true, - privacy: true, - secretName: true, - kubeconfig: true, - clusterResources: true, - cpu: true, - gpu: true, - memory: true, - zone: { - select: { - id: true, - slug: true, - argocdUrl: true, - label: true, - }, - }, + id: true, + slug: true, + argocdUrl: true, + label: true, }, - }); + }, + }, + }) } -export async function updateProjectClusterHistory( - projectId: Project['id'], - clusterIds: Cluster['id'][], -) { - return prisma.$transaction([ - prisma.projectClusterHistory.deleteMany({ - where: { - AND: { - projectId, - clusterId: { notIn: clusterIds }, - }, - }, - }), - prisma.projectClusterHistory.createMany({ - data: clusterIds.map((clusterId) => ({ clusterId, projectId })), - skipDuplicates: true, - }), - ]); +export async function updateProjectClusterHistory(projectId: Project['id'], clusterIds: Cluster['id'][]) { + return prisma.$transaction([ + prisma.projectClusterHistory.deleteMany({ + where: { + AND: { + projectId, + clusterId: { notIn: clusterIds }, + }, + }, + }), + prisma.projectClusterHistory.createMany({ + data: clusterIds.map(clusterId => ({ clusterId, projectId })), + skipDuplicates: true, + }), + ]) } export function getClusterById(id: Cluster['id']) { - return prisma.cluster.findUnique({ - where: { id }, - include: { kubeconfig: true }, - }); + return prisma.cluster.findUnique({ + where: { id }, + include: { kubeconfig: true }, + }) } export function getClusterByIdOrThrow(id: Cluster['id']) { - return prisma.cluster.findUniqueOrThrow({ - where: { id }, - include: { kubeconfig: true, zone: true }, - }); + return prisma.cluster.findUniqueOrThrow({ + where: { id }, + include: { kubeconfig: true, zone: true }, + }) } export function getClusterEnvironments(clusterId: Cluster['id']) { - return prisma.environment.findMany({ - where: { clusterId }, + return prisma.environment.findMany({ + where: { clusterId }, + select: { + name: true, + cpu: true, + gpu: true, + memory: true, + project: { select: { - name: true, - cpu: true, - gpu: true, - memory: true, - project: { - select: { - slug: true, - name: true, - owner: true, - members: true, - }, - }, + slug: true, + name: true, + owner: true, + members: true, }, - }); + }, + }, + }) } export function getClusterDetails(id: Cluster['id']) { - return prisma.cluster.findUniqueOrThrow({ - where: { id }, + return prisma.cluster.findUniqueOrThrow({ + where: { id }, + select: { + createdAt: true, + projects: { select: { - createdAt: true, - projects: { - select: { - id: true, - }, - }, - id: true, - clusterResources: true, - infos: true, - external: true, - label: true, - privacy: true, - kubeconfig: true, - stages: true, - updatedAt: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, + id: true, }, - }); + }, + id: true, + clusterResources: true, + infos: true, + external: true, + label: true, + privacy: true, + kubeconfig: true, + stages: true, + updatedAt: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, + }, + }) } export function getClustersByIds(clusterIds: Cluster['id'][]) { - return prisma.cluster.findMany({ - where: { - id: { in: clusterIds }, - }, - include: { kubeconfig: true }, - }); + return prisma.cluster.findMany({ + where: { + id: { in: clusterIds }, + }, + include: { kubeconfig: true }, + }) } export function getPublicClusters() { - return prisma.cluster.findMany({ - where: { privacy: 'public' }, - include: { zone: true }, - }); + return prisma.cluster.findMany({ + where: { privacy: 'public' }, + include: { zone: true }, + }) } export async function getClusterNamesByZoneId(zoneId: string) { - const clusterNames = await prisma.cluster.findMany({ - where: { zoneId }, - select: { - label: true, - }, - }); - return clusterNames.map(({ label }) => label); + const clusterNames = await prisma.cluster.findMany({ + where: { zoneId }, + select: { + label: true, + }, + }) + return clusterNames.map(({ label }) => label) } export function getClusterByLabel(label: Cluster['label']) { - return prisma.cluster.findUnique({ where: { label } }); + return prisma.cluster.findUnique({ where: { label } }) } export function getClusterByEnvironmentId(id: Environment['id']) { - return prisma.cluster.findMany({ - where: { - environments: { - some: { id }, - }, - }, - include: { kubeconfig: true }, - }); + return prisma.cluster.findMany({ + where: { + environments: { + some: { id }, + }, + }, + include: { kubeconfig: true }, + }) } export function getClustersWithProjectIdAndConfig() { - return prisma.cluster.findMany({ + return prisma.cluster.findMany({ + select: { + id: true, + stages: true, + projects: { + where: { + status: { not: 'archived' }, + }, select: { - id: true, - stages: true, - projects: { - where: { - status: { not: 'archived' }, - }, - select: { - id: true, - name: true, - slug: true, - status: true, - }, - }, - clusterResources: true, - label: true, - infos: true, - privacy: true, - secretName: true, - kubeconfig: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, + id: true, + name: true, + slug: true, + status: true, }, - }); + }, + clusterResources: true, + label: true, + infos: true, + privacy: true, + secretName: true, + kubeconfig: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, + }, + }) } export function listClusters(where: Prisma.ClusterWhereInput) { - return prisma.cluster.findMany({ - where, - select: { - id: true, - label: true, - stages: true, - clusterResources: true, - privacy: true, - infos: true, - external: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, - }, - }); + return prisma.cluster.findMany({ + where, + select: { + id: true, + label: true, + stages: true, + clusterResources: true, + privacy: true, + infos: true, + external: true, + zoneId: true, + cpu: true, + gpu: true, + memory: true, + }, + }) } export async function getProjectsByClusterId(id: Cluster['id']) { - return ( - await prisma.cluster.findUniqueOrThrow({ - where: { id }, - select: { projects: true }, - }) - )?.projects; + return (await prisma.cluster.findUniqueOrThrow({ + where: { id }, + select: { projects: true }, + }))?.projects } export async function listStagesByClusterId(id: Cluster['id']) { - return ( - await prisma.cluster.findUniqueOrThrow({ - where: { id }, - select: { stages: true }, - }) - )?.stages; + return (await prisma.cluster.findUniqueOrThrow({ + where: { id }, + select: { stages: true }, + }))?.stages } -export function createCluster( - data: Omit< - Cluster, - | 'id' - | 'updatedAt' - | 'createdAt' - | 'kubeConfigId' - | 'secretName' - | 'zoneId' - >, - kubeconfig: Pick, - zoneId: string, -) { - return prisma.cluster.create({ - data: { - ...data, - // @ts-ignore - kubeconfig: { create: kubeconfig }, - zone: { - connect: { id: zoneId }, - }, - }, - }); +export function createCluster(data: Omit, kubeconfig: Pick, zoneId: string) { + return prisma.cluster.create({ + data: { + ...data, + // @ts-ignore + kubeconfig: { create: kubeconfig }, + zone: { + connect: { id: zoneId }, + }, + }, + }) } -export function updateCluster( - id: Cluster['id'], - data: Partial< - Omit - >, - kubeconfig: Pick, -) { - return prisma.cluster.update({ - where: { id }, - data: { - ...data, - kubeconfig: { - // @ts-ignore - update: kubeconfig, - }, - }, - }); +export function updateCluster(id: Cluster['id'], data: Partial>, kubeconfig: Pick) { + return prisma.cluster.update({ + where: { id }, + data: { + ...data, + kubeconfig: { + // @ts-ignore + update: kubeconfig, + }, + }, + }) } -export function linkClusterToProjects( - id: Cluster['id'], - projectIds: Project['id'][], -) { - return prisma.cluster.update({ - where: { id }, - data: { - projects: { - connect: projectIds.map((projectId) => ({ id: projectId })), - }, - }, - }); +export function linkClusterToProjects(id: Cluster['id'], projectIds: Project['id'][]) { + return prisma.cluster.update({ + where: { id }, + data: { + projects: { + connect: projectIds.map(projectId => ({ id: projectId })), + }, + }, + }) } -export function linkClusterToStages( - id: Cluster['id'], - stageIds: Stage['id'][], -) { - return prisma.cluster.update({ - where: { id }, - data: { - stages: { - connect: stageIds.map((stageId) => ({ id: stageId })), - }, - }, - }); +export function linkClusterToStages(id: Cluster['id'], stageIds: Stage['id'][]) { + return prisma.cluster.update({ + where: { id }, + data: { + stages: { + connect: stageIds.map(stageId => ({ id: stageId })), + }, + }, + }) } -export function removeClusterFromProject( - id: Cluster['id'], - projectId: Project['id'], -) { - return prisma.cluster.update({ - where: { id }, - data: { - projects: { - disconnect: { - id: projectId, - }, - }, +export function removeClusterFromProject(id: Cluster['id'], projectId: Project['id']) { + return prisma.cluster.update({ + where: { id }, + data: { + projects: { + disconnect: { + id: projectId, }, - }); + }, + }, + }) } -export function removeClusterFromStage( - id: Cluster['id'], - stageId: Stage['id'], -) { - return prisma.cluster.update({ - where: { id }, - data: { - stages: { - disconnect: { - id: stageId, - }, - }, +export function removeClusterFromStage(id: Cluster['id'], stageId: Stage['id']) { + return prisma.cluster.update({ + where: { id }, + data: { + stages: { + disconnect: { + id: stageId, }, - }); + }, + }, + }) } export function deleteCluster(id: Cluster['id']) { - return prisma.cluster.delete({ - where: { id }, - }); + return prisma.cluster.delete({ + where: { id }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts index 365db47a3..c4085a889 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts @@ -1,412 +1,311 @@ -import type { ClusterDetails, Environment } from '@cpn-console/shared'; -import { clusterContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessListMock = vi.spyOn(business, 'listClusters'); -const businessGetDetailsMock = vi.spyOn(business, 'getClusterDetails'); -const businessGetUsageMock = vi.spyOn(business, 'getClusterUsage'); -const businessGetEnvironmentsMock = vi.spyOn( - business, - 'getClusterAssociatedEnvironments', -); -const businessCreateMock = vi.spyOn(business, 'createCluster'); -const businessUpdateMock = vi.spyOn(business, 'updateCluster'); -const businessDeleteMock = vi.spyOn(business, 'deleteCluster'); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ClusterDetails, Environment } from '@cpn-console/shared' +import { clusterContract } from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getUserMockInfos } from '../../utils/mocks' +import { BadRequest400 } from '../../utils/errors' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMock = vi.spyOn(business, 'listClusters') +const businessGetDetailsMock = vi.spyOn(business, 'getClusterDetails') +const businessGetUsageMock = vi.spyOn(business, 'getClusterUsage') +const businessGetEnvironmentsMock = vi.spyOn(business, 'getClusterAssociatedEnvironments') +const businessCreateMock = vi.spyOn(business, 'createCluster') +const businessUpdateMock = vi.spyOn(business, 'updateCluster') +const businessDeleteMock = vi.spyOn(business, 'deleteCluster') describe('test clusterContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - describe('listClusters', () => { - it('as non admin', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - businessListMock.mockResolvedValueOnce([]); - const response = await app - .inject() - .get(clusterContract.listClusters.path) - .end(); - - expect(businessListMock).toHaveBeenCalledWith(user.user.id); - - expect(response.json()).toStrictEqual([]); - expect(response.statusCode).toEqual(200); - }); - it('as admin', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - - businessListMock.mockResolvedValueOnce([]); - const response = await app - .inject() - .get(clusterContract.listClusters.path) - .end(); - - expect(businessListMock).toHaveBeenCalledWith(); - - expect(response.json()).toStrictEqual([]); - expect(response.statusCode).toEqual(200); - }); - }); - - describe('getClusterDetails', () => { - it('should return cluster details', async () => { - const cluster: ClusterDetails = { - id: faker.string.uuid(), - clusterResources: true, - infos: '', - external: false, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - }; - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessGetDetailsMock.mockResolvedValueOnce(cluster); - const response = await app - .inject() - .get( - clusterContract.getClusterDetails.path.replace( - ':clusterId', - cluster.id, - ), - ) - .end(); - - expect(businessGetDetailsMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(cluster); - expect(response.statusCode).toEqual(200); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - clusterContract.getClusterDetails.path.replace( - ':clusterId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessGetDetailsMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('getClusterUsage', () => { - it('should return cluster usage', async () => { - const resources = { - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }), - }; - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessGetUsageMock.mockResolvedValueOnce(resources); - const response = await app - .inject() - .get( - clusterContract.getClusterUsage.path.replace( - ':clusterId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessGetUsageMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(resources); - expect(response.statusCode).toEqual(200); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - clusterContract.getClusterUsage.path.replace( - ':clusterId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessGetUsageMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('getClusterEnvironments', () => { - it('should return cluster environments', async () => { - const envs: Environment[] = []; - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessGetEnvironmentsMock.mockResolvedValueOnce(envs); - const response = await app - .inject() - .get( - clusterContract.getClusterEnvironments.path.replace( - ':clusterId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual([]); - expect(response.statusCode).toEqual(200); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - clusterContract.getClusterEnvironments.path.replace( - ':clusterId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('createCluster', () => { - const cluster: ClusterDetails = { - id: faker.string.uuid(), - clusterResources: true, - infos: '', - external: true, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - }; - - it('should return created cluster', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce(cluster); - const response = await app - .inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end(); - - expect(response.json()).toEqual(cluster); - expect(response.statusCode).toEqual(201); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end(); - - expect(response.statusCode).toEqual(403); - }); - }); - - describe('updateCluster', () => { - const clusterId = faker.string.uuid(); - const cluster: Omit = { - clusterResources: true, - infos: '', - external: false, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - }; - - it('should return created cluster', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce({ - id: clusterId, - ...cluster, - }); - const response = await app - .inject() - .put( - clusterContract.updateCluster.path.replace( - ':clusterId', - clusterId, - ), - ) - .body(cluster) - .end(); - - expect(response.json()).toEqual({ id: clusterId, ...cluster }); - expect(response.statusCode).toEqual(200); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .put( - clusterContract.updateCluster.path.replace( - ':clusterId', - clusterId, - ), - ) - .body(cluster) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - clusterContract.updateCluster.path.replace( - ':clusterId', - clusterId, - ), - ) - .body(cluster) - .end(); - - expect(response.statusCode).toEqual(403); - }); - }); - - describe('deleteCluster', () => { - it('should return empty when delete', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce(null); - const response = await app - .inject() - .delete( - clusterContract.deleteCluster.path.replace( - ':clusterId', - faker.string.uuid(), - ), - ) - .end(); - - expect(response.body).toBeFalsy(); - expect(response.statusCode).toEqual(204); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .delete( - clusterContract.deleteCluster.path.replace( - ':clusterId', - faker.string.uuid(), - ), - ) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - clusterContract.deleteCluster.path.replace( - ':clusterId', - faker.string.uuid(), - ), - ) - .end(); - - expect(response.statusCode).toEqual(403); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + describe('listClusters', () => { + it('as non admin', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + businessListMock.mockResolvedValueOnce([]) + const response = await app.inject() + .get(clusterContract.listClusters.path) + .end() + + expect(businessListMock).toHaveBeenCalledWith(user.user.id) + + expect(response.json()).toStrictEqual([]) + expect(response.statusCode).toEqual(200) + }) + it('as admin', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + + businessListMock.mockResolvedValueOnce([]) + const response = await app.inject() + .get(clusterContract.listClusters.path) + .end() + + expect(businessListMock).toHaveBeenCalledWith() + + expect(response.json()).toStrictEqual([]) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('getClusterDetails', () => { + it('should return cluster details', async () => { + const cluster: ClusterDetails = { + id: faker.string.uuid(), + clusterResources: true, + infos: '', + external: false, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + } + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetDetailsMock.mockResolvedValueOnce(cluster) + const response = await app.inject() + .get(clusterContract.getClusterDetails.path.replace(':clusterId', cluster.id)) + .end() + + expect(businessGetDetailsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(cluster) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(clusterContract.getClusterDetails.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetDetailsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getClusterUsage', () => { + it('should return cluster usage', async () => { + const resources = { + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + } + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetUsageMock.mockResolvedValueOnce(resources) + const response = await app.inject() + .get(clusterContract.getClusterUsage.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetUsageMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(resources) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(clusterContract.getClusterUsage.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetUsageMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getClusterEnvironments', () => { + it('should return cluster environments', async () => { + const envs: Environment[] = [] + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetEnvironmentsMock.mockResolvedValueOnce(envs) + const response = await app.inject() + .get(clusterContract.getClusterEnvironments.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(clusterContract.getClusterEnvironments.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('createCluster', () => { + const cluster: ClusterDetails = { + id: faker.string.uuid(), + clusterResources: true, + infos: '', + external: true, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + } + + it('should return created cluster', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(cluster) + const response = await app.inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end() + + expect(response.json()).toEqual(cluster) + expect(response.statusCode).toEqual(201) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(clusterContract.createCluster.path) + .body(cluster) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('updateCluster', () => { + const clusterId = faker.string.uuid() + const cluster: Omit = { + clusterResources: true, + infos: '', + external: false, + label: faker.string.alpha(), + privacy: 'public', + stageIds: [], + zoneId: faker.string.uuid(), + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + kubeconfig: { + cluster: { tlsServerName: faker.string.alpha() }, + user: {}, + }, + } + + it('should return created cluster', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: clusterId, ...cluster }) + const response = await app.inject() + .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) + .body(cluster) + .end() + + expect(response.json()).toEqual({ id: clusterId, ...cluster }) + expect(response.statusCode).toEqual(200) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) + .body(cluster) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) + .body(cluster) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('deleteCluster', () => { + it('should return empty when delete', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts index 20870a5c2..9824fe104 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts @@ -1,161 +1,125 @@ -import type { AsyncReturnType } from '@cpn-console/shared'; -import { AdminAuthorized, clusterContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import '@old-server/types/index'; -import { authUser } from '@old-server/utils/controller'; +import type { AsyncReturnType } from '@cpn-console/shared' +import { AdminAuthorized, clusterContract } from '@cpn-console/shared' import { - ErrorResType, - Forbidden403, - Unauthorized401, -} from '@old-server/utils/errors'; - -import { - createCluster, - deleteCluster, - getClusterAssociatedEnvironments, - getClusterDetails as getClusterDetailsBusiness, - getClusterUsage, - listClusters, - updateCluster, -} from './business'; - -@Injectable() -export class ClusterRouterService { - constructor(private readonly appService: AppService) {} - - clusterRouter() { - return this.appService.serverInstance.router(clusterContract, { - listClusters: async ({ request: req }) => { - const { adminPermissions, user } = await authUser(req); - - let body: AsyncReturnType = []; - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listClusters(); - } else if (user) { - body = await listClusters(user.id); - } - - return { - status: 200, - body, - }; - }, - - getClusterDetails: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const cluster = await getClusterDetailsBusiness(clusterId); - - return { - status: 200, - body: cluster, - }; - }, - - getClusterUsage: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const usage = await getClusterUsage(clusterId); - - return { - status: 200, - body: usage, - }; - }, - - createCluster: async ({ request: req, body: data }) => { - const { adminPermissions, user } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - const body = await createCluster(data, user.id, req.id); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - getClusterEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const environments = - await getClusterAssociatedEnvironments(clusterId); - - return { - status: 200, - body: environments, - }; - }, - - updateCluster: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - - const clusterId = params.clusterId; - const body = await updateCluster( - data, - clusterId, - user.id, - req.id, - ); - - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - deleteCluster: async ({ - request: req, - params, - query: { force }, - }) => { - const { user, adminPermissions, tokenId } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user?.id && !tokenId) - return new Unauthorized401( - 'Your identity has not been found', - ); - - const clusterId = params.clusterId; - const body = await deleteCluster({ - clusterId, - userId: user?.id, - requestId: req.id, - force, - }); - - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); - } + createCluster, + deleteCluster, + getClusterAssociatedEnvironments, + getClusterDetails as getClusterDetailsBusiness, + getClusterUsage, + listClusters, + updateCluster, +} from './business' +import '@old-server/types/index' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' + +export function clusterRouter() { + return serverInstance.router(clusterContract, { + listClusters: async ({ request: req }) => { + const { adminPermissions, user } = await authUser(req) + + let body: AsyncReturnType = [] + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listClusters() + } else if (user) { + body = await listClusters(user.id) + } + + return { + status: 200, + body, + } + }, + + getClusterDetails: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const clusterId = params.clusterId + const cluster = await getClusterDetailsBusiness(clusterId) + + return { + status: 200, + body: cluster, + } + }, + + getClusterUsage: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const clusterId = params.clusterId + const usage = await getClusterUsage(clusterId) + + return { + status: 200, + body: usage, + } + }, + + createCluster: async ({ request: req, body: data }) => { + const { adminPermissions, user } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + + if (!user) return new Unauthorized401('Require to be requested from user not api key') + const body = await createCluster(data, user.id, req.id) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + getClusterEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const clusterId = params.clusterId + const environments = await getClusterAssociatedEnvironments(clusterId) + + return { + status: 200, + body: environments, + } + }, + + updateCluster: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user) return new Unauthorized401('Require to be requested from user not api key') + + const clusterId = params.clusterId + const body = await updateCluster(data, clusterId, user.id, req.id) + + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + deleteCluster: async ({ request: req, params, query: { force } }) => { + const { user, adminPermissions, tokenId } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user?.id && !tokenId) return new Unauthorized401('Your identity has not been found') + + const clusterId = params.clusterId + const body = await deleteCluster({ + clusterId, + userId: user?.id, + requestId: req.id, + force, + }) + + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts index c31624ff4..36104efa3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts @@ -1,441 +1,353 @@ -import { faker } from '@faker-js/faker'; -import type { - Cluster, - Environment, - Project, - ProjectMembers, - ProjectRole, - Stage, - User, -} from '@prisma/client'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import { hook } from '../../__mocks__/utils/hook-wrapper'; -import { Result } from '../../utils/business'; -import { - checkClusterResources, - checkProjectResources, - createEnvironment, - deleteEnvironment, - getProjectEnvironments, - updateEnvironment, -} from './business'; +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Cluster, Environment, Project, ProjectMembers, ProjectRole, Stage, User } from '@prisma/client' +import prisma from '../../__mocks__/prisma' +import { hook } from '../../__mocks__/utils/hook-wrapper' +import { checkClusterResources, checkProjectResources, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business' +import { Result } from '../../utils/business' vi.mock('../../utils/hook-wrapper', async () => ({ - hook, -})); + hook, +})) const user: User = { - id: faker.string.uuid(), - createdAt: new Date(), - updatedAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - adminRoleIds: [], - type: 'human', - lastLogin: null, -}; + id: faker.string.uuid(), + createdAt: new Date(), + updatedAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + adminRoleIds: [], + type: 'human', + lastLogin: null, +} const project: Project & { - clusters: Pick[]; - members: ProjectMembers[]; - roles: ProjectRole[]; - owner: User; + clusters: Pick[] + members: ProjectMembers[] + roles: ProjectRole[] + owner: User } = { - createdAt: new Date(), - updatedAt: new Date(), - description: '', - everyonePerms: 649n, - id: faker.string.uuid(), - locked: false, - name: faker.string.alphanumeric(8), - status: 'created', - ownerId: faker.string.uuid(), - owner: user, - limitless: false, - hprodCpu: faker.number.int({ min: 0, max: 1000 }), - hprodGpu: faker.number.int({ min: 0, max: 1000 }), - hprodMemory: faker.number.int({ min: 0, max: 1000 }), - prodCpu: faker.number.int({ min: 0, max: 1000 }), - prodGpu: faker.number.int({ min: 0, max: 1000 }), - prodMemory: faker.number.int({ min: 0, max: 1000 }), - clusters: [], - roles: [], - members: [], - slug: faker.string.alphanumeric(8), - lastSuccessProvisionningVersion: faker.string.numeric(), -}; + createdAt: new Date(), + updatedAt: new Date(), + description: '', + everyonePerms: 649n, + id: faker.string.uuid(), + locked: false, + name: faker.string.alphanumeric(8), + status: 'created', + ownerId: faker.string.uuid(), + owner: user, + limitless: false, + hprodCpu: faker.number.int({ min: 0, max: 1000 }), + hprodGpu: faker.number.int({ min: 0, max: 1000 }), + hprodMemory: faker.number.int({ min: 0, max: 1000 }), + prodCpu: faker.number.int({ min: 0, max: 1000 }), + prodGpu: faker.number.int({ min: 0, max: 1000 }), + prodMemory: faker.number.int({ min: 0, max: 1000 }), + clusters: [], + roles: [], + members: [], + slug: faker.string.alphanumeric(8), + lastSuccessProvisionningVersion: faker.string.numeric(), +} describe('test environment business', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('getProjectEnvironments', () => { - it('should query environment for projectId', async () => { - prisma.environment.findMany.mockResolvedValue([]); - const projectId = faker.string.uuid(); - await getProjectEnvironments(projectId); - - expect(prisma.environment.findMany).toHaveBeenCalledTimes(1); - }); - }); - - describe('createEnvironment', () => { - const clusterId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const env = { name: 'new-env' }; - it('should create environment and trigger hook', async () => { - const requestId = faker.string.uuid(); - const stageId = faker.string.uuid(); - - prisma.environment.create.mockResolvedValue({ - clusterId, - } as Environment); - hook.project.upsert.mockResolvedValue({ - results: {}, - project: { ...project }, - }); - - const result = await createEnvironment({ - userId: user.id, - projectId: project.id, - name: env.name, - cpu: 0.1, - gpu: 0.5, - memory: 2.0, - clusterId, - stageId, - requestId, - }); - - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(prisma.environment.create).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeTruthy(); - }); - - it('should create environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid(); - - prisma.environment.create.mockResolvedValue({ - clusterId, - } as Environment); - hook.project.upsert.mockResolvedValue({ - results: { failed: true }, - project: { ...project }, - }); - - const result = await createEnvironment({ - userId: user.id, - projectId: project.id, - name: env.name, - cpu: 0.1, - gpu: 0.5, - memory: 2.0, - clusterId, - stageId, - requestId, - }); - - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(prisma.environment.create).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeFalsy(); - }); - }); - - describe('updateEnvironment', () => { - it('should update environment and trigger hook', async () => { - const requestId = faker.string.uuid(); - const environmentId = faker.string.uuid(); - - prisma.environment.update.mockResolvedValue({ - projectId: project.id, - } as Environment); - hook.project.upsert.mockResolvedValue({ - results: {}, - project: { ...project }, - }); - - const result = await updateEnvironment({ - user, - environmentId, - requestId, - cpu: 2.0, - gpu: 4.0, - memory: 12.5, - }); - - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(prisma.environment.update).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeTruthy(); - }); - - it('should update environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid(); - const environmentId = faker.string.uuid(); - - prisma.environment.update.mockResolvedValue({ - projectId: project.id, - } as Environment); - hook.project.upsert.mockResolvedValue({ - results: { failed: true }, - project: { ...project }, - }); - - const result = await updateEnvironment({ - user, - environmentId, - requestId, - cpu: 2.0, - gpu: 4.0, - memory: 12.5, - }); - - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(prisma.environment.update).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeFalsy(); - }); - }); - - describe('deleteEnvironment', () => { - it('should delete environment and trigger hook', async () => { - const requestId = faker.string.uuid(); - const environmentId = faker.string.uuid(); - - prisma.environment.delete.mockResolvedValue({ - projectId: project.id, - } as Environment); - hook.project.upsert.mockResolvedValue({ - results: {}, - project: { ...project }, - }); - - const result = await deleteEnvironment({ - environmentId, - userId: user.id, - projectId: project.id, - requestId, - }); - - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(prisma.environment.delete).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeTruthy(); - }); - - it('should delete environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid(); - const environmentId = faker.string.uuid(); - - prisma.environment.delete.mockResolvedValue({ - projectId: project.id, - } as Environment); - hook.project.upsert.mockResolvedValue({ - results: { failed: true }, - project: { ...project }, - }); - - const result = await deleteEnvironment({ - environmentId, - userId: user.id, - projectId: project.id, - requestId, - }); - - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(prisma.environment.delete).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeFalsy(); - }); - }); - - describe('checkClusterResources', () => { - it('should authorize cluster not yet configured', async () => { - const cluster: Cluster = { - cpu: 0, - gpu: 0, - memory: 0, - } as Cluster; - const result = await checkClusterResources( - { cpu: 1, gpu: 0, memory: 1 }, - cluster, - ); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeTruthy(); - }); - it('should authorize cluster not yet used', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster; - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 0, - gpu: 0, - memory: 0, - }, - } as any); - const result = await checkClusterResources( - { cpu: 8, gpu: 0, memory: 7 }, - cluster, - ); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeTruthy(); - }); - it('should authorize cluster used but not full', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster; - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 2, - gpu: 0, - memory: 2, - }, - } as any); - const result = await checkClusterResources( - { cpu: 8, gpu: 0, memory: 6 }, - cluster, - ); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeTruthy(); - }); - it('should refuse cluster without enough space', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster; - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 5, - gpu: 0, - memory: 5, - }, - } as any); - const result = await checkClusterResources( - { cpu: 8, gpu: 0, memory: 6 }, - cluster, - ); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeFalsy(); - expect(result.error).toEqual( - 'Le cluster ne dispose pas de suffisamment de ressources : CPU, Mémoire.', - ); - }); - it('should refuse cluster without GPU', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster; - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 2, - gpu: 0, - memory: 2, - }, - } as any); - const result = await checkClusterResources( - { cpu: 2, gpu: 1, memory: 2 }, - cluster, - ); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeFalsy(); - expect(result.error).toEqual( - 'Le cluster ne dispose pas de suffisamment de ressources : GPU.', - ); - }); - }); - - describe('checkProjectResources', () => { - const prodStage: Stage = { - id: faker.string.uuid(), - name: 'prod', - }; - const hprodStage: Stage = { - id: faker.string.uuid(), - name: 'hprod', - }; - it('should authorize prod deployment for project with hprod resource but no prod resources', async () => { - const project: Project = { - hprodCpu: 10, - hprodGpu: 10, - hprodMemory: 10, - prodCpu: 0, - prodGpu: 0, - prodMemory: 0, - } as Project; - prisma.stage.findUnique.mockResolvedValue(prodStage); - prisma.stage.findMany.mockResolvedValue([prodStage]); - const result = await checkProjectResources( - { cpu: 1, gpu: 0, memory: 1, stageId: prodStage.id }, - project, - ); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeTruthy(); - }); - it('should refuse hprod deployment for project with hprod resource but no prod resources', async () => { - const project: Project = { - hprodCpu: 10, - hprodGpu: 10, - hprodMemory: 10, - prodCpu: 0, - prodGpu: 0, - prodMemory: 0, - } as Project; - prisma.stage.findUnique.mockResolvedValue(hprodStage); - prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]); - prisma.environment.aggregate.mockResolvedValue({ - _sum: { cpu: 0, gpu: 0, memory: 0 }, - } as any); - const result = await checkProjectResources( - { cpu: 20, gpu: 20, memory: 20, stageId: hprodStage.id }, - project, - ); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeFalsy(); - expect(result.error).toEqual( - 'Le projet ne dispose pas de suffisamment de ressources : CPU, GPU, Mémoire.', - ); - }); - it('should refuse overloading hprod deployment', async () => { - const project: Project = { - hprodCpu: 20, - hprodGpu: 20, - hprodMemory: 20, - prodCpu: 10, - prodGpu: 10, - prodMemory: 10, - } as Project; - prisma.stage.findUnique.mockResolvedValue(hprodStage); - prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]); - prisma.environment.aggregate.mockResolvedValue({ - _sum: { cpu: 15, gpu: 15, memory: 15 }, - } as any); - const result = await checkProjectResources( - { cpu: 5, gpu: 6, memory: 5, stageId: hprodStage.id }, - project, - ); - expect(result).toBeInstanceOf(Result); - expect(result.success).toBeFalsy(); - expect(result.error).toEqual( - 'Le projet ne dispose pas de suffisamment de ressources : GPU.', - ); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('getProjectEnvironments', () => { + it('should query environment for projectId', async () => { + prisma.environment.findMany.mockResolvedValue([]) + const projectId = faker.string.uuid() + await getProjectEnvironments(projectId) + + expect(prisma.environment.findMany).toHaveBeenCalledTimes(1) + }) + }) + + describe('createEnvironment', () => { + const clusterId = faker.string.uuid() + const stageId = faker.string.uuid() + const env = { name: 'new-env' } + it('should create environment and trigger hook', async () => { + const requestId = faker.string.uuid() + const stageId = faker.string.uuid() + + prisma.environment.create.mockResolvedValue({ clusterId } as Environment) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const result = await createEnvironment({ + userId: user.id, + projectId: project.id, + name: env.name, + cpu: 0.1, + gpu: 0.5, + memory: 2.0, + clusterId, + stageId, + requestId, + }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + + it('should create environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid() + + prisma.environment.create.mockResolvedValue({ clusterId } as Environment) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const result = await createEnvironment({ + userId: user.id, + projectId: project.id, + name: env.name, + cpu: 0.1, + gpu: 0.5, + memory: 2.0, + clusterId, + stageId, + requestId, + }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + }) + }) + + describe('updateEnvironment', () => { + it('should update environment and trigger hook', async () => { + const requestId = faker.string.uuid() + const environmentId = faker.string.uuid() + + prisma.environment.update.mockResolvedValue({ projectId: project.id } as Environment) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const result = await updateEnvironment({ + user, + environmentId, + requestId, + cpu: 2.0, + gpu: 4.0, + memory: 12.5, + }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.update).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + + it('should update environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid() + const environmentId = faker.string.uuid() + + prisma.environment.update.mockResolvedValue({ projectId: project.id } as Environment) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const result = await updateEnvironment({ + user, + environmentId, + requestId, + cpu: 2.0, + gpu: 4.0, + memory: 12.5, + }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.update).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + }) + }) + + describe('deleteEnvironment', () => { + it('should delete environment and trigger hook', async () => { + const requestId = faker.string.uuid() + const environmentId = faker.string.uuid() + + prisma.environment.delete.mockResolvedValue({ projectId: project.id } as Environment) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const result = await deleteEnvironment({ environmentId, userId: user.id, projectId: project.id, requestId }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.delete).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + + it('should delete environment and trigger hook but hooks failed', async () => { + const requestId = faker.string.uuid() + const environmentId = faker.string.uuid() + + prisma.environment.delete.mockResolvedValue({ projectId: project.id } as Environment) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const result = await deleteEnvironment({ environmentId, userId: user.id, projectId: project.id, requestId }) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(prisma.environment.delete).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + }) + }) + + describe('checkClusterResources', () => { + it('should authorize cluster not yet configured', async () => { + const cluster: Cluster = { + cpu: 0, + gpu: 0, + memory: 0, + } as Cluster + const result = await checkClusterResources({ cpu: 1, gpu: 0, memory: 1 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + it('should authorize cluster not yet used', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 0, + gpu: 0, + memory: 0, + }, + } as any) + const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 7 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + it('should authorize cluster used but not full', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 2, + gpu: 0, + memory: 2, + }, + } as any) + const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 6 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + it('should refuse cluster without enough space', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 5, + gpu: 0, + memory: 5, + }, + } as any) + const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 6 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + expect(result.error).toEqual('Le cluster ne dispose pas de suffisamment de ressources : CPU, Mémoire.') + }) + it('should refuse cluster without GPU', async () => { + const cluster: Cluster = { + cpu: 10, + gpu: 0, + memory: 8, + } as Cluster + prisma.environment.aggregate.mockResolvedValue({ + _sum: { + cpu: 2, + gpu: 0, + memory: 2, + }, + } as any) + const result = await checkClusterResources({ cpu: 2, gpu: 1, memory: 2 }, cluster) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + expect(result.error).toEqual('Le cluster ne dispose pas de suffisamment de ressources : GPU.') + }) + }) + + describe('checkProjectResources', () => { + const prodStage: Stage = { + id: faker.string.uuid(), + name: 'prod', + } + const hprodStage: Stage = { + id: faker.string.uuid(), + name: 'hprod', + } + it('should authorize prod deployment for project with hprod resource but no prod resources', async () => { + const project: Project = { + hprodCpu: 10, + hprodGpu: 10, + hprodMemory: 10, + prodCpu: 0, + prodGpu: 0, + prodMemory: 0, + } as Project + prisma.stage.findUnique.mockResolvedValue(prodStage) + prisma.stage.findMany.mockResolvedValue([prodStage]) + const result = await checkProjectResources({ cpu: 1, gpu: 0, memory: 1, stageId: prodStage.id }, project) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeTruthy() + }) + it('should refuse hprod deployment for project with hprod resource but no prod resources', async () => { + const project: Project = { + hprodCpu: 10, + hprodGpu: 10, + hprodMemory: 10, + prodCpu: 0, + prodGpu: 0, + prodMemory: 0, + } as Project + prisma.stage.findUnique.mockResolvedValue(hprodStage) + prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]) + prisma.environment.aggregate.mockResolvedValue({ + _sum: { cpu: 0, gpu: 0, memory: 0 }, + } as any) + const result = await checkProjectResources({ cpu: 20, gpu: 20, memory: 20, stageId: hprodStage.id }, project) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + expect(result.error).toEqual('Le projet ne dispose pas de suffisamment de ressources : CPU, GPU, Mémoire.') + }) + it('should refuse overloading hprod deployment', async () => { + const project: Project = { + hprodCpu: 20, + hprodGpu: 20, + hprodMemory: 20, + prodCpu: 10, + prodGpu: 10, + prodMemory: 10, + } as Project + prisma.stage.findUnique.mockResolvedValue(hprodStage) + prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]) + prisma.environment.aggregate.mockResolvedValue({ + _sum: { cpu: 15, gpu: 15, memory: 15 }, + } as any) + const result = await checkProjectResources({ cpu: 5, gpu: 6, memory: 5, stageId: hprodStage.id }, project) + expect(result).toBeInstanceOf(Result) + expect(result.success).toBeFalsy() + expect(result.error).toEqual('Le projet ne dispose pas de suffisamment de ressources : GPU.') + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts index 4dc313c87..83131d848 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts @@ -1,388 +1,300 @@ -import prisma from '@old-server/prisma'; +import type { Cluster, Environment, Project, Stage, User } from '@prisma/client' import { - addLogs, - deleteEnvironment as deleteEnvironmentQuery, - getEnvironmentsByProjectId, - initializeEnvironment, - updateEnvironment as updateEnvironmentQuery, -} from '@old-server/resources/queries-index'; -import type { Resources, UserDetails } from '@old-server/types/index'; -import { Result } from '@old-server/utils/business'; -import { hook } from '@old-server/utils/hook-wrapper'; -import type { - Cluster, - Environment, - Project, - Stage, - User, -} from '@prisma/client'; + addLogs, + deleteEnvironment as deleteEnvironmentQuery, + getEnvironmentsByProjectId, + initializeEnvironment, + updateEnvironment as updateEnvironmentQuery, +} from '@old-server/resources/queries-index' +import type { Resources, UserDetails } from '@old-server/types/index' +import { hook } from '@old-server/utils/hook-wrapper' +import prisma from '@old-server/prisma' +import { Result } from '@old-server/utils/business' export function getProjectEnvironments(projectId: Project['id']) { - return getEnvironmentsByProjectId(projectId); + return getEnvironmentsByProjectId(projectId) } // Routes logic interface CreateEnvironmentParam { - userId: User['id']; - projectId: Project['id']; - name: Environment['name']; - cpu: Environment['cpu']; - gpu: Environment['gpu']; - memory: Environment['memory']; - clusterId: Environment['clusterId']; - stageId: Stage['id']; - requestId: string; + userId: User['id'] + projectId: Project['id'] + name: Environment['name'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] + clusterId: Environment['clusterId'] + stageId: Stage['id'] + requestId: string } interface CreateEnvironmentResult { - id: Environment['id']; - createdAt: Date; - updatedAt: Date; - projectId: Project['id']; - name: Environment['name']; - cpu: Environment['cpu']; - gpu: Environment['gpu']; - memory: Environment['memory']; - clusterId: Environment['clusterId']; - stageId: Stage['id']; + id: Environment['id'] + createdAt: Date + updatedAt: Date + projectId: Project['id'] + name: Environment['name'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] + clusterId: Environment['clusterId'] + stageId: Stage['id'] } export async function createEnvironment({ - userId, - projectId, - name, - cpu, - gpu, - memory, - clusterId, - stageId, - requestId, + userId, + projectId, + name, + cpu, + gpu, + memory, + clusterId, + stageId, + requestId, }: CreateEnvironmentParam): Promise> { - const environment = await initializeEnvironment({ - projectId, - name, - cpu, - gpu, - memory, - clusterId, - stageId, - }); + const environment = await initializeEnvironment({ projectId, name, cpu, gpu, memory, clusterId, stageId }) - const { results } = await hook.project.upsert(projectId); - await addLogs({ - action: 'Create Environment', - data: results, - userId, - requestId, - projectId, - }); - if (results.failed) { - return Result.fail( - "Echec des services à la création de l'environnement", - ); - } + const { results } = await hook.project.upsert(projectId) + await addLogs({ action: 'Create Environment', data: results, userId, requestId, projectId }) + if (results.failed) { + return Result.fail('Echec des services à la création de l\'environnement') + } - return Result.succeed({ - ...environment, - stageId, - }); + return Result.succeed({ + ...environment, + stageId, + }) } interface UpdateEnvironmentParam { - user: UserDetails; - environmentId: Environment['id']; - cpu: Environment['cpu']; - gpu: Environment['gpu']; - memory: Environment['memory']; - requestId: string; + user: UserDetails + environmentId: Environment['id'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] + requestId: string } export async function updateEnvironment({ - user, - environmentId, - requestId, + user, + environmentId, + requestId, + cpu, + gpu, + memory, +}: UpdateEnvironmentParam) { + const env = await updateEnvironmentQuery({ + id: environmentId, cpu, gpu, memory, -}: UpdateEnvironmentParam) { - const env = await updateEnvironmentQuery({ - id: environmentId, - cpu, - gpu, - memory, - }); - const { results } = await hook.project.upsert(env.projectId); - await addLogs({ - action: 'Update Environment', - data: results, - userId: user.id, - requestId, - projectId: env.projectId, - }); - if (results.failed) { - return Result.fail( - "Echec des services à la mise à jour de l'environnement", - ); - } + }) + const { results } = await hook.project.upsert(env.projectId) + await addLogs({ action: 'Update Environment', data: results, userId: user.id, requestId, projectId: env.projectId }) + if (results.failed) { + return Result.fail('Echec des services à la mise à jour de l\'environnement') + } - return Result.succeed(env); + return Result.succeed(env) } interface DeleteEnvironmentParam { - userId?: User['id']; - environmentId: Environment['id']; - projectId: Project['id']; - requestId: string; + userId?: User['id'] + environmentId: Environment['id'] + projectId: Project['id'] + requestId: string } export async function deleteEnvironment({ - userId, - environmentId, - projectId, - requestId, + userId, + environmentId, + projectId, + requestId, }: DeleteEnvironmentParam) { - const env = await deleteEnvironmentQuery(environmentId); + const env = await deleteEnvironmentQuery(environmentId) - const { results } = await hook.project.upsert(projectId); - await addLogs({ - action: 'Delete Environment', - data: results, - userId, - requestId, - projectId: env.projectId, - }); - if (results.failed) { - return Result.fail( - "Echec des services à la suppression de l'environnement", - ); - } - return Result.succeed(null); + const { results } = await hook.project.upsert(projectId) + await addLogs({ action: 'Delete Environment', data: results, userId, requestId, projectId: env.projectId }) + if (results.failed) { + return Result.fail('Echec des services à la suppression de l\'environnement') + } + return Result.succeed(null) } export async function checkEnvironmentCreate(input: { - clusterId: Cluster['id']; - projectId: Project['id']; - name: Environment['name']; - stageId: Stage['id']; - cpu: Environment['cpu']; - gpu: Environment['gpu']; - memory: Environment['memory']; + clusterId: Cluster['id'] + projectId: Project['id'] + name: Environment['name'] + stageId: Stage['id'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] }): Promise> { - const errorMessages: string[] = []; - const [stage, sameNameEnvironment, cluster] = await Promise.all([ - input.stageId - ? prisma.stage.findUnique({ where: { id: input.stageId } }) - : undefined, - input.name - ? prisma.environment.findUnique({ - where: { - projectId_name: { - projectId: input.projectId, - name: input.name, - }, - }, - }) - : undefined, - input.clusterId - ? prisma.cluster.findFirst({ - where: { - OR: [ - { - // un cluster public - id: input.clusterId, - privacy: 'public', - }, - { - id: input.clusterId, // un cluster dédié rattaché au projet - privacy: 'dedicated', - projects: { some: { id: input.projectId } }, - }, - ], - }, - }) - : undefined, - ]); - if (sameNameEnvironment) - errorMessages.push("Ce nom d'environnement est déjà pris."); - if (!stage) errorMessages.push("Type d'environnment invalide."); - if (!cluster) { - errorMessages.push('Cluster invalide.'); - } else { - const resourceCheckResult = await checkClusterResources(input, cluster); - if (resourceCheckResult.isError) { - errorMessages.push(resourceCheckResult.error); - } - const project = await prisma.project.findUniqueOrThrow({ - where: { id: input.projectId }, - }); - const projectCheckResult = await checkProjectResources(input, project); - if (projectCheckResult.isError) { - errorMessages.push(projectCheckResult.error); - } + const errorMessages: string[] = [] + const [stage, sameNameEnvironment, cluster] = await Promise.all([ + input.stageId + ? prisma.stage.findUnique({ where: { id: input.stageId } }) + : undefined, + input.name + ? prisma.environment.findUnique({ where: { projectId_name: { projectId: input.projectId, name: input.name } } }) + : undefined, + input.clusterId + ? prisma.cluster.findFirst({ + where: { + OR: [{ // un cluster public + id: input.clusterId, + privacy: 'public', + }, { + id: input.clusterId, // un cluster dédié rattaché au projet + privacy: 'dedicated', + projects: { some: { id: input.projectId } }, + }], + }, + }) + : undefined, + ]) + if (sameNameEnvironment) errorMessages.push('Ce nom d\'environnement est déjà pris.') + if (!stage) errorMessages.push('Type d\'environnment invalide.') + if (!cluster) { + errorMessages.push('Cluster invalide.') + } else { + const resourceCheckResult = await checkClusterResources(input, cluster) + if (resourceCheckResult.isError) { + errorMessages.push(resourceCheckResult.error) } - if (errorMessages.length > 0) { - return Result.fail(errorMessages.join('\n')); + const project = await prisma.project.findUniqueOrThrow({ where: { id: input.projectId } }) + const projectCheckResult = await checkProjectResources(input, project) + if (projectCheckResult.isError) { + errorMessages.push(projectCheckResult.error) } - return Result.succeed(true); + } + if (errorMessages.length > 0) { + return Result.fail(errorMessages.join('\n')) + } + return Result.succeed(true) } -export async function checkClusterResources( - input: { - cpu: Environment['cpu']; - gpu: Environment['gpu']; - memory: Environment['memory']; +export async function checkClusterResources(input: { + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] +}, cluster: Cluster): Promise> { + if (cluster.cpu === 0 && cluster.memory === 0) { + // Unconfigured cluster + return Result.succeed(true) + } + const unsufficientResource = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { cpu: cluster.cpu, gpu: cluster.gpu, memory: cluster.memory }, + where: { + cluster: { + id: cluster.id, + }, }, - cluster: Cluster, -): Promise> { - if (cluster.cpu === 0 && cluster.memory === 0) { - // Unconfigured cluster - return Result.succeed(true); - } - const unsufficientResource = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { cpu: cluster.cpu, gpu: cluster.gpu, memory: cluster.memory }, - where: { - cluster: { - id: cluster.id, - }, - }, - }); - if (unsufficientResource.length > 0) { - return Result.fail( - `Le cluster ne dispose pas de suffisamment de ressources : ${unsufficientResource.join(', ')}.`, - ); - } - return Result.succeed(true); + }) + if (unsufficientResource.length > 0) { + return Result.fail(`Le cluster ne dispose pas de suffisamment de ressources : ${unsufficientResource.join(', ')}.`) + } + return Result.succeed(true) } -export async function checkProjectResources( - input: { - cpu: Environment['cpu']; - gpu: Environment['gpu']; - memory: Environment['memory']; - stageId: Environment['stageId']; - }, - project: Project, -): Promise> { - if (project.limitless) { - // No limits - return Result.succeed(true); - } - const stage = await prisma.stage.findUnique({ - where: { id: input.stageId }, - }); - const prodStages = await prisma.stage.findMany({ - select: { id: true }, - where: { name: 'prod' }, - }); - let overflowResources: string[]; - if (stage?.name === 'prod') { - overflowResources = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { - cpu: project.prodCpu, - gpu: project.prodGpu, - memory: project.prodMemory, - }, - where: { - projectId: project.id, - stageId: { - in: prodStages.map((s) => s.id), - }, - }, - }); - } else { - // hprod - overflowResources = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { - cpu: project.hprodCpu, - gpu: project.hprodGpu, - memory: project.hprodMemory, - }, - where: { - projectId: project.id, - stageId: { - notIn: prodStages.map((s) => s.id), - }, - }, - }); - } - if (overflowResources.length > 0) { - return Result.fail( - `Le projet ne dispose pas de suffisamment de ressources : ${overflowResources.join(', ')}.`, - ); - } - return Result.succeed(true); +export async function checkProjectResources(input: { + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] + stageId: Environment['stageId'] +}, project: Project): Promise> { + if (project.limitless) { + // No limits + return Result.succeed(true) + } + const stage = await prisma.stage.findUnique({ where: { id: input.stageId } }) + const prodStages = await prisma.stage.findMany({ select: { id: true }, where: { name: 'prod' } }) + let overflowResources: string[] + if (stage?.name === 'prod') { + overflowResources = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { cpu: project.prodCpu, gpu: project.prodGpu, memory: project.prodMemory }, + where: { + projectId: project.id, + stageId: { + in: prodStages.map(s => s.id), + }, + }, + }) + } else { // hprod + overflowResources = await getOverflowResources({ + request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, + limit: { cpu: project.hprodCpu, gpu: project.hprodGpu, memory: project.hprodMemory }, + where: { + projectId: project.id, + stageId: { + notIn: prodStages.map(s => s.id), + }, + }, + }) + } + if (overflowResources.length > 0) { + return Result.fail(`Le projet ne dispose pas de suffisamment de ressources : ${overflowResources.join(', ')}.`) + } + return Result.succeed(true) } export async function checkEnvironmentUpdate(input: { - environmentId: Environment['id']; - cpu: Environment['cpu']; - gpu: Environment['gpu']; - memory: Environment['memory']; + environmentId: Environment['id'] + cpu: Environment['cpu'] + gpu: Environment['gpu'] + memory: Environment['memory'] }): Promise> { - const environment = await prisma.environment.findUniqueOrThrow({ - select: { cluster: true, projectId: true, stageId: true }, - where: { id: input.environmentId }, - }); - const cluster = await prisma.cluster.findUniqueOrThrow({ - where: { id: environment.cluster.id }, - }); - const errorMessages: string[] = []; - const resourceCheckResult = await checkClusterResources(input, cluster); - if (resourceCheckResult.isError) { - errorMessages.push(resourceCheckResult.error); - } - const project = await prisma.project.findUniqueOrThrow({ - where: { id: environment.projectId }, - }); - const projectCheckResult = await checkProjectResources( - { stageId: environment.stageId, ...input }, - project, - ); - if (projectCheckResult.isError) { - errorMessages.push(projectCheckResult.error); - } - if (errorMessages.length > 0) { - return Result.fail(errorMessages.join('\n')); - } - return Result.succeed(true); + const environment = await prisma.environment.findUniqueOrThrow({ + select: { cluster: true, projectId: true, stageId: true }, + where: { id: input.environmentId }, + }) + const cluster = await prisma.cluster.findUniqueOrThrow({ + where: { id: environment.cluster.id }, + }) + const errorMessages: string[] = [] + const resourceCheckResult = await checkClusterResources(input, cluster) + if (resourceCheckResult.isError) { + errorMessages.push(resourceCheckResult.error) + } + const project = await prisma.project.findUniqueOrThrow({ where: { id: environment.projectId } }) + const projectCheckResult = await checkProjectResources({ stageId: environment.stageId, ...input }, project) + if (projectCheckResult.isError) { + errorMessages.push(projectCheckResult.error) + } + if (errorMessages.length > 0) { + return Result.fail(errorMessages.join('\n')) + } + return Result.succeed(true) } -export async function getOverflowResources({ - request, - limit, - where, -}: { - request: Resources; - limit: Resources; - where: any; +export async function getOverflowResources({ request, limit, where }: { + request: Resources + limit: Resources + where: any }): Promise { - if (limit.cpu === 0 && limit.memory === 0) { - // Unconfigured project prod resources - return []; - } - const environmentResources = await prisma.environment.aggregate({ - _sum: { - memory: true, - cpu: true, - gpu: true, - }, - where, - }); - const unsufficientResource: string[] = []; - if ((environmentResources._sum.cpu ?? 0) + request.cpu > limit.cpu) { - unsufficientResource.push('CPU'); - } - if ((environmentResources._sum.gpu ?? 0) + request.gpu > limit.gpu) { - unsufficientResource.push('GPU'); - } - if ( - (environmentResources._sum.memory ?? 0) + request.memory > - limit.memory - ) { - unsufficientResource.push('Mémoire'); - } - return unsufficientResource; + if (limit.cpu === 0 && limit.memory === 0) { + // Unconfigured project prod resources + return [] + } + const environmentResources = await prisma.environment.aggregate({ + _sum: { + memory: true, + cpu: true, + gpu: true, + }, + where, + }) + const unsufficientResource: string[] = [] + if ((environmentResources._sum.cpu ?? 0) + request.cpu > limit.cpu) { + unsufficientResource.push('CPU') + } + if ((environmentResources._sum.gpu ?? 0) + request.gpu > limit.gpu) { + unsufficientResource.push('GPU') + } + if ((environmentResources._sum.memory ?? 0) + request.memory > limit.memory) { + unsufficientResource.push('Mémoire') + } + return unsufficientResource } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts index 78c0bc070..78a68da69 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts @@ -1,113 +1,98 @@ -import prisma from '@old-server/prisma'; -import type { Environment, Prisma, Project } from '@prisma/client'; +import type { Environment, Prisma, Project } from '@prisma/client' +import prisma from '@old-server/prisma' // SELECT export function getEnvironmentByIdOrThrow(id: Environment['id']) { - return prisma.environment.findUniqueOrThrow({ - where: { id }, - include: { stage: true }, - }); + return prisma.environment.findUniqueOrThrow({ where: { id }, include: { stage: true } }) } export function getEnvironmentInfos(id: Environment['id']) { - return prisma.environment.findUniqueOrThrow({ - where: { id }, - include: { - project: { - select: { - owner: true, - name: true, - id: true, - status: true, - repositories: { - where: { isInfra: true }, - }, - locked: true, - clusters: { - select: { - id: true, - label: true, - privacy: true, - clusterResources: true, - }, - }, - }, + return prisma.environment.findUniqueOrThrow({ + where: { id }, + include: { + project: { + select: { + owner: true, + name: true, + id: true, + status: true, + repositories: { + where: { isInfra: true }, + }, + locked: true, + clusters: { + select: { + id: true, + label: true, + privacy: true, + clusterResources: true, }, - stage: true, + }, }, - }); + }, + stage: true, + }, + }) } export async function getEnvironmentsByProjectId(projectId: Project['id']) { - return prisma.environment.findMany({ - where: { projectId }, - include: { - stage: true, - }, - }); + return prisma.environment.findMany({ + where: { projectId }, + include: { + stage: true, + }, + }) } export function getEnvironmentByIdWithCluster(id: Environment['id']) { - return prisma.environment.findUnique({ - where: { id }, - include: { - cluster: { - include: { kubeconfig: true }, - }, - }, - }); + return prisma.environment.findUnique({ + where: { id }, + include: { + cluster: { + include: { kubeconfig: true }, + }, + }, + }) } // INSERT -export function initializeEnvironment( - data: Prisma.EnvironmentUncheckedCreateInput, -) { - return prisma.environment.create({ - data, +export function initializeEnvironment(data: Prisma.EnvironmentUncheckedCreateInput) { + return prisma.environment.create({ + data, + include: { + project: { include: { - project: { - include: { - repositories: { - where: { isInfra: true }, - }, - }, - }, + repositories: { + where: { isInfra: true }, + }, }, - }); + }, + }, + }) } -export function updateEnvironment({ - id, - cpu, - gpu, - memory, -}: { - id: Environment['id']; - cpu: Environment['cpu']; - gpu: Environment['gpu']; - memory: Environment['memory']; -}) { - return prisma.environment.update({ - where: { - id, - }, - data: { - cpu, - gpu, - memory, - }, - }); +export function updateEnvironment({ id, cpu, gpu, memory }: { id: Environment['id'], cpu: Environment['cpu'], gpu: Environment['gpu'], memory: Environment['memory'] }) { + return prisma.environment.update({ + where: { + id, + }, + data: { + cpu, + gpu, + memory, + }, + }) } // DELETE export function deleteEnvironment(id: Environment['id']) { - return prisma.environment.delete({ - where: { id }, - }); + return prisma.environment.delete({ + where: { id }, + }) } export function deleteAllEnvironmentForProject(id: Project['id']) { - return prisma.environment.deleteMany({ - where: { projectId: id }, - }); + return prisma.environment.deleteMany({ + where: { projectId: id }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts index c7f725405..764b6c6f1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts @@ -1,590 +1,372 @@ -import { - type Environment, - PROJECT_PERMS, - environmentContract, -} from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { - atDates, - getProjectMockInfos, - getUserMockInfos, -} from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessGetProjectEnvironmentsMock = vi.spyOn( - business, - 'getProjectEnvironments', -); -const businessCreateEnvironmentMock = vi.spyOn(business, 'createEnvironment'); -const businessUpdateEnvironmentMock = vi.spyOn(business, 'updateEnvironment'); -const businessDeleteEnvironmentMock = vi.spyOn(business, 'deleteEnvironment'); -const businessCheckEnvironmentCreateMock = vi.spyOn( - business, - 'checkEnvironmentCreate', -); -const businessCheckEnvironmentUpdateMock = vi.spyOn( - business, - 'checkEnvironmentUpdate', -); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { type Environment, PROJECT_PERMS, environmentContract } from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { atDates, getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetProjectEnvironmentsMock = vi.spyOn(business, 'getProjectEnvironments') +const businessCreateEnvironmentMock = vi.spyOn(business, 'createEnvironment') +const businessUpdateEnvironmentMock = vi.spyOn(business, 'updateEnvironment') +const businessDeleteEnvironmentMock = vi.spyOn(business, 'deleteEnvironment') +const businessCheckEnvironmentCreateMock = vi.spyOn(business, 'checkEnvironmentCreate') +const businessCheckEnvironmentUpdateMock = vi.spyOn(business, 'checkEnvironmentUpdate') describe('environmentRouter tests', () => { - let projectId: string; - let environmentId: string; - let environmentData: Omit; - + let projectId: string + let environmentId: string + let environmentData: Omit + + beforeEach(() => { + vi.resetAllMocks() + projectId = faker.string.uuid() + environmentId = faker.string.uuid() + environmentData = { + projectId, + name: 'envname', + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + clusterId: faker.string.uuid(), + stageId: faker.string.uuid(), + } + }) + + describe('listEnvironments', () => { + it('should return environments for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetProjectEnvironmentsMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(environmentContract.listEnvironments.path) + .query({ projectId }) + .end() + + expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledWith(projectId) + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should return empty for non member of projectId query ', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(environmentContract.listEnvironments.path) + .query({ projectId }) + .end() + + expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledTimes(0) + expect(response.json()).toEqual([]) + }) + }) + + describe('createEnvironment', () => { + it('should create environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ success: true }) + businessCreateEnvironmentMock.mockResolvedValueOnce({ + success: true, + data: { id: environmentId, ...environmentData, ...atDates }, + }) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.json()).toMatchObject({ id: environmentId, ...environmentData }) + expect(response.statusCode).toEqual(201) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(403) + }) + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ success: true, message: 'pas d erreur' }) + businessCreateEnvironmentMock.mockResolvedValueOnce({ isError: true, message: 'une erreur' }) + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(500) + }) + it('should pass invalid reason error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ isError: true, message: 'une erreur' }) + const response = await app.inject() + .post(environmentContract.createEnvironment.path) + .body(environmentData) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('updateEnvironment', () => { + let updateData: { cpu: number, gpu: number, memory: number } beforeEach(() => { - vi.resetAllMocks(); - projectId = faker.string.uuid(); - environmentId = faker.string.uuid(); - environmentData = { - projectId, - name: 'envname', - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - clusterId: faker.string.uuid(), - stageId: faker.string.uuid(), - }; - }); - - describe('listEnvironments', () => { - it('should return environments for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessGetProjectEnvironmentsMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .get(environmentContract.listEnvironments.path) - .query({ projectId }) - .end(); - - expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledWith( - projectId, - ); - expect(response.statusCode).toEqual(200); - expect(response.json()).toEqual([]); - }); - - it('should return empty for non member of projectId query ', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(environmentContract.listEnvironments.path) - .query({ projectId }) - .end(); - - expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledTimes(0); - expect(response.json()).toEqual([]); - }); - }); - - describe('createEnvironment', () => { - it('should create environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ - success: true, - }); - businessCreateEnvironmentMock.mockResolvedValueOnce({ - success: true, - data: { id: environmentId, ...environmentData, ...atDates }, - }); - - const response = await app - .inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end(); - - expect(response.json()).toMatchObject({ - id: environmentId, - ...environmentData, - }); - expect(response.statusCode).toEqual(201); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end(); - - expect(response.statusCode).toEqual(403); - }); - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ - success: true, - message: 'pas d erreur', - }); - businessCreateEnvironmentMock.mockResolvedValueOnce({ - isError: true, - message: 'une erreur', - }); - const response = await app - .inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end(); - - expect(response.statusCode).toEqual(500); - }); - it('should pass invalid reason error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ - isError: true, - message: 'une erreur', - }); - const response = await app - .inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end(); - - expect(response.statusCode).toEqual(400); - }); - }); - - describe('updateEnvironment', () => { - let updateData: { cpu: number; gpu: number; memory: number }; - beforeEach(() => { - updateData = { - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ - min: 0, - max: 10, - fractionDigits: 1, - }), - }; - }); - it('should update environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ - success: true, - value: true, - }); - businessUpdateEnvironmentMock.mockResolvedValueOnce({ - success: true, - data: { id: environmentId, ...environmentData, ...atDates }, - }); - - const response = await app - .inject() - .put( - environmentContract.updateEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .body(updateData) - .end(); - - expect(response.json()).toMatchObject({ - id: environmentId, - ...environmentData, - }); - expect(response.statusCode).toEqual(200); - }); - - it('should return 403 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - environmentContract.updateEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - environmentContract.updateEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - environmentContract.updateEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - environmentContract.updateEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 404 if not permited', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - environmentContract.updateEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(404); - }); - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateEnvironmentMock.mockResolvedValueOnce({ - isError: true, - value: 'une erreur', - }); - const response = await app - .inject() - .put( - environmentContract.updateEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(500); - }); - it('should pass invalid reason error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ - isError: true, - value: 'une erreur', - }); - const response = await app - .inject() - .put( - environmentContract.updateEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(400); - }); - }); - - describe('deleteEnvironment', () => { - it('should delete environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteEnvironmentMock.mockResolvedValueOnce({ - success: true, - }); - - const response = await app - .inject() - .delete( - environmentContract.deleteEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(204); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - environmentContract.deleteEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - environmentContract.deleteEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - environmentContract.deleteEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - environmentContract.deleteEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteEnvironmentMock.mockResolvedValueOnce({ - isError: true, - value: 'une erreur', - }); - const response = await app - .inject() - .delete( - environmentContract.deleteEnvironment.path.replace( - ':environmentId', - environmentId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(500); - }); - }); -}); + updateData = { + cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), + } + }) + it('should update environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ success: true, value: true }) + businessUpdateEnvironmentMock.mockResolvedValueOnce({ success: true, data: { id: environmentId, ...environmentData, ...atDates } }) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.json()).toMatchObject({ id: environmentId, ...environmentData }) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 404 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(404) + }) + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateEnvironmentMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(500) + }) + it('should pass invalid reason error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) + const response = await app.inject() + .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('deleteEnvironment', () => { + it('should delete environment for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteEnvironmentMock.mockResolvedValueOnce({ success: true }) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(204) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteEnvironmentMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) + const response = await app.inject() + .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) + .end() + + expect(response.statusCode).toEqual(500) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts index 4a462ea0b..699f63471 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts @@ -1,156 +1,109 @@ -import { ProjectAuthorized, environmentContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { - BadRequest400, - Forbidden403, - Internal500, - NotFound404, - Unauthorized401, -} from '@old-server/utils/errors'; +import { ProjectAuthorized, environmentContract } from '@cpn-console/shared' +import { checkEnvironmentCreate, checkEnvironmentUpdate, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401 } from '@old-server/utils/errors' -import { - checkEnvironmentCreate, - checkEnvironmentUpdate, - createEnvironment, - deleteEnvironment, - getProjectEnvironments, - updateEnvironment, -} from './business'; +export function environmentRouter() { + return serverInstance.router(environmentContract, { + listEnvironments: async ({ request: req, query }) => { + const projectId = query.projectId + const perms = await authUser(req, { id: projectId }) -@Injectable() -export class EnvironmentRouterService { - constructor(private readonly appService: AppService) {} + const environments = ProjectAuthorized.ListEnvironments(perms) + ? await getProjectEnvironments(projectId) + : [] - environmentRouter() { - return this.appService.serverInstance.router(environmentContract, { - listEnvironments: async ({ request: req, query }) => { - const projectId = query.projectId; - const perms = await authUser(req, { id: projectId }); + return { + status: 200, + body: environments, + } + }, - const environments = ProjectAuthorized.ListEnvironments(perms) - ? await getProjectEnvironments(projectId) - : []; + createEnvironment: async ({ request: req, body: requestBody }) => { + const projectId = requestBody.projectId + const perms = await authUser(req, { id: projectId }) - return { - status: 200, - body: environments, - }; - }, + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - createEnvironment: async ({ request: req, body: requestBody }) => { - const projectId = requestBody.projectId; - const perms = await authUser(req, { id: projectId }); + const checkCreateResult = await checkEnvironmentCreate({ ...requestBody }) + if (checkCreateResult.isError) return new BadRequest400(checkCreateResult.error) - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); + const result = await createEnvironment({ + userId: perms.user.id, + projectId, + name: requestBody.name, + clusterId: requestBody.clusterId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + stageId: requestBody.stageId, + requestId: req.id, + }) + if (result.isError) { + return new Internal500(result.error) + } + return { + status: 201, + body: result.data, + } + }, - const checkCreateResult = await checkEnvironmentCreate({ - ...requestBody, - }); - if (checkCreateResult.isError) - return new BadRequest400(checkCreateResult.error); + updateEnvironment: async ({ request: req, body: requestBody, params }) => { + const { environmentId } = params + const perms = await authUser(req, { environmentId }) + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!ProjectAuthorized.ListEnvironments(perms)) return new NotFound404() + if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - const result = await createEnvironment({ - userId: perms.user.id, - projectId, - name: requestBody.name, - clusterId: requestBody.clusterId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - stageId: requestBody.stageId, - requestId: req.id, - }); - if (result.isError) { - return new Internal500(result.error); - } - return { - status: 201, - body: result.data, - }; - }, + const checkUpdateResult = await checkEnvironmentUpdate({ environmentId, ...requestBody }) + if (checkUpdateResult.isError) return new BadRequest400(checkUpdateResult.error) - updateEnvironment: async ({ - request: req, - body: requestBody, - params, - }) => { - const { environmentId } = params; - const perms = await authUser(req, { environmentId }); - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if (!ProjectAuthorized.ListEnvironments(perms)) - return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); + const result = await updateEnvironment({ + user: perms.user, + environmentId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + requestId: req.id, + }) + if (result.isError) { + return new Internal500(result.error) + } + return { + status: 200, + body: result.data, + } + }, - const checkUpdateResult = await checkEnvironmentUpdate({ - environmentId, - ...requestBody, - }); - if (checkUpdateResult.isError) - return new BadRequest400(checkUpdateResult.error); + deleteEnvironment: async ({ request: req, params }) => { + const { environmentId } = params + const perms = await authUser(req, { environmentId }) + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - const result = await updateEnvironment({ - user: perms.user, - environmentId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - requestId: req.id, - }); - if (result.isError) { - return new Internal500(result.error); - } - return { - status: 200, - body: result.data, - }; - }, + const result = await deleteEnvironment({ + userId: perms.user?.id, + environmentId, + requestId: req.id, + projectId: perms.projectId, + }) + if (result.isError) { + return new Internal500(result.error) + } - deleteEnvironment: async ({ request: req, params }) => { - const { environmentId } = params; - const perms = await authUser(req, { environmentId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const result = await deleteEnvironment({ - userId: perms.user?.id, - environmentId, - requestId: req.id, - projectId: perms.projectId, - }); - if (result.isError) { - return new Internal500(result.error); - } - - return { - status: 204, - body: result.data, - }; - }, - }); - } + return { + status: 204, + body: result.data, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts index 99af62fec..7082924b6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts @@ -1,174 +1,49 @@ -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import type { FastifyInstance } from 'fastify'; +import type { FastifyInstance } from 'fastify' +import { serverInstance } from '@old-server/app' -import { AdminRoleRouterService } from './admin-role/router'; -import { AdminTokenRouterService } from './admin-token/router'; -import { ClusterRouterService } from './cluster/router'; -import { EnvironmentRouterService } from './environment/router'; -import { LogRouterService } from './log/router'; -import { ProjectMemberRouterService } from './project-member/router'; -import { ProjectRoleRouterService } from './project-role/router'; -import { ProjectServiceRouterService } from './project-service/router'; -import { ProjectRouterService } from './project/router'; -import { RepositoryRouterService } from './repository/router'; -import { ServiceChainRouterService } from './service-chain/router'; -import { ServiceMonitorRouterService } from './service-monitor/router'; -import { StageRouterService } from './stage/router'; -import { SystemConfigRouterService } from './system/config/router'; -import { SystemRouterService } from './system/router'; -import { SystemSettingsRouterService } from './system/settings/router'; -import { UserRouterService } from './user/router'; -import { UserTokensRouterService } from './user/tokens/router'; -import { ZoneRouterService } from './zone/router'; +import { adminRoleRouter } from './admin-role/router' +import { adminTokenRouter } from './admin-token/router' +import { clusterRouter } from './cluster/router' +import { environmentRouter } from './environment/router' +import { logRouter } from './log/router' +import { personalAccessTokenRouter } from './user/tokens/router' +import { pluginConfigRouter } from './system/config/router' +import { projectMemberRouter } from './project-member/router' +import { projectRoleRouter } from './project-role/router' +import { projectRouter } from './project/router' +import { projectServiceRouter } from './project-service/router' +import { repositoryRouter } from './repository/router' +import { serviceChainRouter } from './service-chain/router' +import { serviceMonitorRouter } from './service-monitor/router' +import { stageRouter } from './stage/router' +import { systemRouter } from './system/router' +import { systemSettingsRouter } from './system/settings/router' +import { userRouter } from './user/router' +import { zoneRouter } from './zone/router' -@Injectable() -export class ResourcesService { - constructor( - private readonly appService: AppService, - private readonly adminRoleRouterService: AdminRoleRouterService, - private readonly adminTokenRouterService: AdminTokenRouterService, - private readonly clusterRouterService: ClusterRouterService, - private readonly environmentRouterService: EnvironmentRouterService, - private readonly logRouterService: LogRouterService, - private readonly projectMemberRouterService: ProjectMemberRouterService, - private readonly projectRoleRouterService: ProjectRoleRouterService, - private readonly projectServiceRouterService: ProjectServiceRouterService, - private readonly projectRouterService: ProjectRouterService, - private readonly repositoryRouterService: RepositoryRouterService, - private readonly serviceChainRouterService: ServiceChainRouterService, - private readonly serviceMonitorRouterService: ServiceMonitorRouterService, - private readonly stageRouterService: StageRouterService, - private readonly systemConfigRouterService: SystemConfigRouterService, - private readonly systemRouterService: SystemRouterService, - private readonly systemSettingsRouterService: SystemSettingsRouterService, - private readonly userRouterService: UserRouterService, - private readonly userTokensRouterService: UserTokensRouterService, - private readonly zoneRouterService: ZoneRouterService, - ) {} - - // relax validation schema if NO_VALIDATION env var is set to true. - // /!\ It can lead to security leaks !!!! - validateTrue = { - responseValidation: process.env.NO_VALIDATION !== 'true', - }; - - apiRouter() { - return async (app: FastifyInstance) => { - await app.register( - this.appService.serverInstance.plugin( - this.adminRoleRouterService.adminRoleRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.adminTokenRouterService.adminTokenRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.clusterRouterService.clusterRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.serviceChainRouterService.serviceChainRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.environmentRouterService.environmentRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.logRouterService.logRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.userTokensRouterService.personalAccessTokenRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.projectRouterService.projectRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.projectMemberRouterService.projectMemberRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.projectRoleRouterService.projectRoleRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.projectServiceRouterService.projectServiceRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.repositoryRouterService.repositoryRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.serviceMonitorRouterService.serviceMonitorRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.systemConfigRouterService.pluginConfigRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.stageRouterService.stageRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.systemRouterService.systemRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.systemSettingsRouterService.systemSettingsRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.userRouterService.userRouter(), - ), - this.validateTrue, - ); - await app.register( - this.appService.serverInstance.plugin( - this.zoneRouterService.zoneRouter(), - ), - this.validateTrue, - ); - }; - } +// relax validation schema if NO_VALIDATION env var is set to true. +// /!\ It can lead to security leaks !!!! +const validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' } +export function apiRouter() { + return async (app: FastifyInstance) => { + await app.register(serverInstance.plugin(adminRoleRouter()), validateTrue) + await app.register(serverInstance.plugin(adminTokenRouter()), validateTrue) + await app.register(serverInstance.plugin(clusterRouter()), validateTrue) + await app.register(serverInstance.plugin(serviceChainRouter()), validateTrue) + await app.register(serverInstance.plugin(environmentRouter()), validateTrue) + await app.register(serverInstance.plugin(logRouter()), validateTrue) + await app.register(serverInstance.plugin(personalAccessTokenRouter()), validateTrue) + await app.register(serverInstance.plugin(projectRouter()), validateTrue) + await app.register(serverInstance.plugin(projectMemberRouter()), validateTrue) + await app.register(serverInstance.plugin(projectRoleRouter()), validateTrue) + await app.register(serverInstance.plugin(projectServiceRouter()), validateTrue) + await app.register(serverInstance.plugin(repositoryRouter()), validateTrue) + await app.register(serverInstance.plugin(serviceMonitorRouter()), validateTrue) + await app.register(serverInstance.plugin(pluginConfigRouter()), validateTrue) + await app.register(serverInstance.plugin(stageRouter()), validateTrue) + await app.register(serverInstance.plugin(systemRouter()), validateTrue) + await app.register(serverInstance.plugin(systemSettingsRouter()), validateTrue) + await app.register(serverInstance.plugin(userRouter()), validateTrue) + await app.register(serverInstance.plugin(zoneRouter()), validateTrue) + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts index 3075b0dc7..751c67991 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts @@ -1,57 +1,42 @@ -import { faker } from '@faker-js/faker'; -import { describe, expect, it } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import { getLogs } from './business'; +import { describe, expect, it } from 'vitest' +import { faker } from '@faker-js/faker' +import prisma from '../../__mocks__/prisma' +import { getLogs } from './business' describe('test log business', () => { - it('should map filter (clean logs)', async () => { - const dbLogs = [ - { - data: { args: {} }, - createdAt: new Date(), - updatedAt: new Date(), - userId: null, - action: 'Action', - id: faker.string.uuid(), - }, - ]; - const query = { - limit: 10, - offset: 10, - clean: true, - projectId: undefined, - }; - prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]); - const [_total, logs] = await getLogs(query); + it('should map filter (clean logs)', async () => { + const dbLogs = [{ + data: { args: {} }, + createdAt: new Date(), + updatedAt: new Date(), + userId: null, + action: 'Action', + id: faker.string.uuid(), + }] + const query = { limit: 10, offset: 10, clean: true, projectId: undefined } + prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]) + const [_total, logs] = await getLogs(query) - expect(logs[0]).not.haveOwnProperty('requestId'); - expect(logs[0].data).not.haveOwnProperty('results'); - expect(logs[0].data).not.haveOwnProperty('args'); - expect(logs[0].data).not.haveOwnProperty('config'); - }); + expect(logs[0]).not.haveOwnProperty('requestId') + expect(logs[0].data).not.haveOwnProperty('results') + expect(logs[0].data).not.haveOwnProperty('args') + expect(logs[0].data).not.haveOwnProperty('config') + }) - it('should not filter (admin logs)', async () => { - const dbLogs = [ - { - data: { args: {} }, - createdAt: new Date(), - updatedAt: new Date(), - userId: null, - action: 'Action', - id: faker.string.uuid(), - }, - ]; - const query = { - limit: 10, - offset: 10, - clean: false, - projectId: undefined, - }; - prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]); - const [_total, logs] = await getLogs(query); + it('should not filter (admin logs)', async () => { + const dbLogs = [{ + data: { args: {} }, + createdAt: new Date(), + updatedAt: new Date(), + userId: null, + action: 'Action', + id: faker.string.uuid(), + }] + const query = { limit: 10, offset: 10, clean: false, projectId: undefined } + prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]) + const [_total, logs] = await getLogs(query) - expect(logs[0].data).haveOwnProperty('args'); - expect(logs[0].data).not.haveOwnProperty('config'); - }); -}); + expect(logs[0].data).haveOwnProperty('args') + expect(logs[0].data).not.haveOwnProperty('config') + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts index 02f5d6941..1ebfa48f8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts @@ -1,17 +1,13 @@ -import type { logContract } from '@cpn-console/shared'; -import { CleanLogSchema } from '@cpn-console/shared'; -import { getAllLogs } from '@old-server/resources/queries-index'; +import type { logContract } from '@cpn-console/shared' +import { CleanLogSchema } from '@cpn-console/shared' +import { getAllLogs } from '@old-server/resources/queries-index' -export async function getLogs({ - offset, - limit, - projectId, - clean, -}: typeof logContract.getLogs.query._type) { - const [total, logs] = await getAllLogs({ - skip: offset, - take: limit, - where: { projectId }, - }); - return [total, clean ? logs.map((log) => CleanLogSchema.parse(log)) : logs]; +export async function getLogs({ offset, limit, projectId, clean }: typeof logContract.getLogs.query._type) { + const [total, logs] = await getAllLogs({ skip: offset, take: limit, where: { projectId } }) + return [ + total, + clean + ? logs.map(log => CleanLogSchema.parse(log)) + : logs, + ] } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts index b4266368c..9ca1ea52d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts @@ -1,69 +1,57 @@ -import { exclude } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; -import type { Log, Prisma, Project, User } from '@prisma/client'; +import type { Log, Prisma, Project, User } from '@prisma/client' +import { exclude } from '@cpn-console/shared' +import prisma from '@old-server/prisma' // SELECT export function getAllLogsForUser(user: User, offset = 0) { - return prisma.log.findMany({ - where: { userId: user.id }, - take: 100, - skip: offset, - }); + return prisma.log.findMany({ + where: { userId: user.id }, + take: 100, + skip: offset, + }) } -export function getAllLogs({ - skip = 0, - take = 5, - where, -}: Prisma.LogFindManyArgs) { - return prisma.$transaction([ - prisma.log.count({ where }), - prisma.log.findMany({ - orderBy: { - createdAt: 'desc', - }, - skip, - take, - where, - }), - ]); +export function getAllLogs({ skip = 0, take = 5, where }: Prisma.LogFindManyArgs) { + return prisma.$transaction([ + prisma.log.count({ where }), + prisma.log.findMany({ + orderBy: { + createdAt: 'desc', + }, + skip, + take, + where, + }), + ]) } // CREATE interface AddLogsArgs { - action: Log['action']; - data: Record; - userId?: User['id'] | null; - requestId: string; - projectId?: Project['id']; + action: Log['action'] + data: Record + userId?: User['id'] | null + requestId: string + projectId?: Project['id'] } -export function addLogs({ - action, - data, - requestId, - userId = null, - projectId, -}: AddLogsArgs) { - return prisma.log.create({ - data: { - action, - userId, - data: exclude(data, ['cluster', 'user', 'newCreds', 'apis']), - requestId, - projectId, - }, - }); +export function addLogs({ action, data, requestId, userId = null, projectId }: AddLogsArgs) { + return prisma.log.create({ + data: { + action, + userId, + data: exclude(data, ['cluster', 'user', 'newCreds', 'apis']), + requestId, + projectId, + }, + }) } // TECH -export function _createLog( - data: Parameters[0]['create'], -) { - return prisma.log.upsert({ - where: { - id: data.id, - }, - create: data, - update: data, - }); +export function _createLog(data: Parameters[0]['create']) { + return prisma.log.upsert({ + where: { + id: data.id, + }, + create: data, + update: data, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts index e661aa013..1e50ba574 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts @@ -1,110 +1,93 @@ -import { logContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessGetLogsMock = vi.spyOn(business, 'getLogs'); +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { logContract } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetLogsMock = vi.spyOn(business, 'getLogs') describe('test logContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('getLogs', () => { - it('should return logs for admin', async () => { - const user = getUserMockInfos(true); - const logs = []; - const total = 1; - - authUserMock.mockResolvedValueOnce(user); - businessGetLogsMock.mockResolvedValueOnce([total, logs]); - - const response = await app - .inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0 }) - .end(); - - expect(authUserMock).toHaveBeenCalledTimes(1); - expect(businessGetLogsMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual({ total, logs }); - expect(response.statusCode).toEqual(200); - }); - - it('should return 403 for non-admin, no projectId', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0 }) - .end(); - - expect(authUserMock).toHaveBeenCalledTimes(1); - expect(businessGetLogsMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - - it('should return logs for non-admin, with projectId', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 1n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - const projectId = faker.string.uuid(); - - const logs = []; - const total = 1; - - businessGetLogsMock.mockResolvedValueOnce([total, logs]); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0, projectId, clean: false }) - .end(); - - expect(authUserMock).toHaveBeenCalledTimes(1); - expect(businessGetLogsMock).toHaveBeenCalledWith({ - clean: true, - limit: 10, - offset: 0, - projectId, - }); - expect(response.statusCode).toEqual(200); - }); - - it('should not return logs for non-admin, with projectId', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - const projectId = faker.string.uuid(); - - const logs = []; - const total = 1; - - businessGetLogsMock.mockResolvedValueOnce([total, logs]); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0, projectId, clean: false }) - .end(); - - expect(response.statusCode).toEqual(403); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('getLogs', () => { + it('should return logs for admin', async () => { + const user = getUserMockInfos(true) + const logs = [] + const total = 1 + + authUserMock.mockResolvedValueOnce(user) + businessGetLogsMock.mockResolvedValueOnce([total, logs]) + + const response = await app.inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0 }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetLogsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ total, logs }) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for non-admin, no projectId', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0 }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetLogsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + + it('should return logs for non-admin, with projectId', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 1n }) + const user = getUserMockInfos(false, undefined, projectPerms) + const projectId = faker.string.uuid() + + const logs = [] + const total = 1 + + businessGetLogsMock.mockResolvedValueOnce([total, logs]) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0, projectId, clean: false }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetLogsMock).toHaveBeenCalledWith({ clean: true, limit: 10, offset: 0, projectId }) + expect(response.statusCode).toEqual(200) + }) + + it('should not return logs for non-admin, with projectId', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + const projectId = faker.string.uuid() + + const logs = [] + const total = 1 + + businessGetLogsMock.mockResolvedValueOnce([total, logs]) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(logContract.getLogs.path) + .query({ limit: 10, offset: 0, projectId, clean: false }) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts index 9a21d8dec..9449b012d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts @@ -1,46 +1,32 @@ -import type { CleanLog, Log, XOR } from '@cpn-console/shared'; -import { AdminAuthorized, logContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import type { - UserProfile, - UserProjectProfile, -} from '@old-server/utils/controller'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403 } from '@old-server/utils/errors'; +import type { CleanLog, Log, XOR } from '@cpn-console/shared' +import { AdminAuthorized, logContract } from '@cpn-console/shared' +import { getLogs } from './business' +import { serverInstance } from '@old-server/app' +import type { UserProfile, UserProjectProfile } from '@old-server/utils/controller' +import { authUser } from '@old-server/utils/controller' +import { Forbidden403 } from '@old-server/utils/errors' -import { getLogs } from './business'; +export function logRouter() { + return serverInstance.router(logContract, { + // Récupérer des logs + getLogs: async ({ request: req, query }) => { + const perms: XOR = query.projectId + ? await authUser(req, { id: query.projectId }) + : await authUser(req) -@Injectable() -export class LogRouterService { - constructor(private readonly appService: AppService) {} + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { + if (!perms.projectPermissions) { + return new Forbidden403() + } + query.clean = true + } - logRouter() { - return this.appService.serverInstance.router(logContract, { - // Récupérer des logs - getLogs: async ({ request: req, query }) => { - const perms: XOR = - query.projectId - ? await authUser(req, { id: query.projectId }) - : await authUser(req); + const [total, logs] = await getLogs(query) as [number, unknown[]] as [number, Array] - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { - if (!perms.projectPermissions) { - return new Forbidden403(); - } - query.clean = true; - } - - const [total, logs] = (await getLogs(query)) as [ - number, - unknown[], - ] as [number, Array]; - - return { - status: 200, - body: { total, logs }, - }; - }, - }); - } + return { + status: 200, + body: { total, logs }, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts index 18a4fb46b..84a2c3758 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts @@ -1,105 +1,60 @@ -import type { XOR, projectMemberContract } from '@cpn-console/shared'; -import { UserSchema } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; +import type { Project, User } from '@prisma/client' +import type { XOR, projectMemberContract } from '@cpn-console/shared' +import { UserSchema } from '@cpn-console/shared' +import { logViaSession } from '../user/business' import { - addLogs, - deleteMember, - listMembers as listMembersQuery, - upsertMember, -} from '@old-server/resources/queries-index'; -import { BadRequest400, NotFound404 } from '@old-server/utils/errors'; -import { hook } from '@old-server/utils/hook-wrapper'; -import type { Project, User } from '@prisma/client'; - -import { logViaSession } from '../user/business'; - -export const listMembers = async (projectId: Project['id']) => - listMembersQuery(projectId); - -export async function addMember( - projectId: Project['id'], - user: XOR<{ userId: string }, { email: string }>, - requestorId: User['id'], - requestId: string, - projectOwnerId: Project['ownerId'], -) { - let userInDb: User | undefined | null; - - if (user.userId) { - userInDb = await prisma.user.findUnique({ - where: { id: user.userId, type: 'human' }, - }); - } else if (user.email) { - userInDb = await prisma.user.findUnique({ - where: { email: user.email, type: 'human' }, - }); - } else { - return new BadRequest400( - 'Veuillez spécifiez au moins un userId ou un email', - ); + addLogs, + deleteMember, + listMembers as listMembersQuery, + upsertMember, +} from '@old-server/resources/queries-index' +import prisma from '@old-server/prisma' +import { BadRequest400, NotFound404 } from '@old-server/utils/errors' +import { hook } from '@old-server/utils/hook-wrapper' + +export const listMembers = async (projectId: Project['id']) => listMembersQuery(projectId) + +export async function addMember(projectId: Project['id'], user: XOR<{ userId: string }, { email: string }>, requestorId: User['id'], requestId: string, projectOwnerId: Project['ownerId']) { + let userInDb: User | undefined | null + + if (user.userId) { + userInDb = await prisma.user.findUnique({ where: { id: user.userId, type: 'human' } }) + } else if (user.email) { + userInDb = await prisma.user.findUnique({ where: { email: user.email, type: 'human' } }) + } else { + return new BadRequest400('Veuillez spécifiez au moins un userId ou un email') + } + if (userInDb) { + if (userInDb.id === projectOwnerId) return new BadRequest400('Le owner ne peut pas être ajouté à cette liste') + } else if (user.email) { + const hookReply = await hook.user.retrieveUserByEmail(user.email) + await addLogs({ action: 'Retrieve User By Email', data: hookReply, userId: requestorId, requestId }) + if (hookReply.failed) { + throw new BadRequest400('Echec de la recherche auprès des services externes') } - if (userInDb) { - if (userInDb.id === projectOwnerId) - return new BadRequest400( - 'Le owner ne peut pas être ajouté à cette liste', - ); - } else if (user.email) { - const hookReply = await hook.user.retrieveUserByEmail(user.email); - await addLogs({ - action: 'Retrieve User By Email', - data: hookReply, - userId: requestorId, - requestId, - }); - if (hookReply.failed) { - throw new BadRequest400( - 'Echec de la recherche auprès des services externes', - ); - } - const retrievedUser = hookReply.results.keycloak?.user; - if (!retrievedUser) return new BadRequest400('Utilisateur introuvable'); - const userValidated = UserSchema.pick({ - email: true, - firstName: true, - lastName: true, - id: true, - }).safeParse(retrievedUser); - if (!userValidated.success) - return new BadRequest400( - "L'utilisateur trouvé ne remplit pas les conditions de vérification", - ); - const logResults = await logViaSession({ - ...userValidated.data, - groups: [], - }); - userInDb = logResults.user; - } else { - return new NotFound404(); - } - - await upsertMember({ projectId, userId: userInDb.id, roleIds: [] }); - return listMembers(projectId); + const retrievedUser = hookReply.results.keycloak?.user + if (!retrievedUser) return new BadRequest400('Utilisateur introuvable') + const userValidated = UserSchema.pick({ email: true, firstName: true, lastName: true, id: true }).safeParse(retrievedUser) + if (!userValidated.success) return new BadRequest400('L\'utilisateur trouvé ne remplit pas les conditions de vérification') + const logResults = await logViaSession({ ...userValidated.data, groups: [] }) + userInDb = logResults.user + } else { + return new NotFound404() + } + + await upsertMember({ projectId, userId: userInDb.id, roleIds: [] }) + return listMembers(projectId) } -export async function patchMembers( - projectId: Project['id'], - members: typeof projectMemberContract.patchMembers.body._type, -) { - for (const member of members) { - await upsertMember({ - projectId, - userId: member.userId, - roleIds: member.roles, - }); - } - return listMembers(projectId); +export async function patchMembers(projectId: Project['id'], members: typeof projectMemberContract.patchMembers.body._type) { + for (const member of members) { + await upsertMember({ projectId, userId: member.userId, roleIds: member.roles }) + } + return listMembers(projectId) } -export async function removeMember( - projectId: Project['id'], - userId: User['id'], -) { - await deleteMember({ projectId, userId }); - return listMembers(projectId); +export async function removeMember(projectId: Project['id'], userId: User['id']) { + await deleteMember({ projectId, userId }) + return listMembers(projectId) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts index 6cb1a10c6..478e1a4b0 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts @@ -1,34 +1,33 @@ -import prisma from '@old-server/prisma'; -import type { Prisma, Project } from '@prisma/client'; +import type { + Prisma, -export const listMembers = (projectId: Project['id']) => - prisma.projectMembers.findMany({ - where: { projectId }, - include: { user: true }, - }); + Project, +} from '@prisma/client' + +import prisma from '@old-server/prisma' + +export const listMembers = (projectId: Project['id']) => prisma.projectMembers.findMany({ where: { projectId }, include: { user: true } }) export function upsertMember(data: Prisma.ProjectMembersUncheckedCreateInput) { - return prisma.projectMembers.upsert({ - where: { - projectId_userId: { - userId: data.userId, - projectId: data.projectId, - }, - }, - create: data, - update: { - roleIds: data.roleIds, - }, - include: { user: true }, - }); + return prisma.projectMembers.upsert({ + where: { + projectId_userId: { + userId: data.userId, + projectId: data.projectId, + }, + }, + create: data, + update: { + roleIds: data.roleIds, + }, + include: { user: true }, + }) } -export function deleteMember( - data: Prisma.ProjectMembersWhereUniqueInput['projectId_userId'], -) { - return prisma.projectMembers.delete({ - where: { - projectId_userId: data, - }, - }); +export function deleteMember(data: Prisma.ProjectMembersWhereUniqueInput['projectId_userId']) { + return prisma.projectMembers.delete({ + where: { + projectId_userId: data, + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts index 9f35d4887..9edee86a7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts @@ -1,456 +1,294 @@ -import type { Member } from '@cpn-console/shared'; -import { PROJECT_PERMS, projectMemberContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessListMembersMock = vi.spyOn(business, 'listMembers'); -const businessAddMemberMock = vi.spyOn(business, 'addMember'); -const businessPatchMembersMock = vi.spyOn(business, 'patchMembers'); -const businessRemoveMemberMock = vi.spyOn(business, 'removeMember'); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Member } from '@cpn-console/shared' +import { PROJECT_PERMS, projectMemberContract } from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' +import { BadRequest400 } from '../../utils/errors' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMembersMock = vi.spyOn(business, 'listMembers') +const businessAddMemberMock = vi.spyOn(business, 'addMember') +const businessPatchMembersMock = vi.spyOn(business, 'patchMembers') +const businessRemoveMemberMock = vi.spyOn(business, 'removeMember') describe('projectMemberRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - const projectId = faker.string.uuid(); - const userId = faker.string.uuid(); - - describe('listMembers', () => { - it('should return members for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessListMembersMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .get( - projectMemberContract.listMembers.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(businessListMembersMock).toHaveBeenCalledWith(projectId); - expect(response.statusCode).toEqual(200); - expect(response.json()).toEqual([]); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - projectMemberContract.listMembers.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(404); - }); - }); - - describe('addMember', () => { - const memberData: Partial = { - userId: faker.string.uuid(), - }; - - it('should add member for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - const newMember = { - ...memberData, - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - roleIds: [], - }; - - businessAddMemberMock.mockResolvedValueOnce([newMember]); - - const response = await app - .inject() - .post( - projectMemberContract.addMember.path.replace( - ':projectId', - projectId, - ), - ) - .body(memberData) - .end(); - - expect(response.json()).toEqual([newMember]); - expect(response.statusCode).toEqual(201); - }); - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - businessAddMemberMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - - const response = await app - .inject() - .post( - projectMemberContract.addMember.path.replace( - ':projectId', - projectId, - ), - ) - .body(memberData) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectMemberContract.addMember.path.replace( - ':projectId', - projectId, - ), - ) - .body(memberData) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectMemberContract.addMember.path.replace( - ':projectId', - projectId, - ), - ) - .body(memberData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectMemberContract.addMember.path.replace( - ':projectId', - projectId, - ), - ) - .body(memberData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - }); - - describe('patchMembers', () => { - const patchData = [{ userId: faker.string.uuid(), roles: [] }]; - - it('should patch members for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessPatchMembersMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .patch( - projectMemberContract.patchMembers.path.replace( - ':projectId', - projectId, - ), - ) - .body(patchData) - .end(); - - expect(response.json()).toEqual([]); - expect(response.statusCode).toEqual(200); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch( - projectMemberContract.patchMembers.path.replace( - ':projectId', - projectId, - ), - ) - .body(patchData) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch( - projectMemberContract.patchMembers.path.replace( - ':projectId', - projectId, - ), - ) - .body(patchData) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch( - projectMemberContract.patchMembers.path.replace( - ':projectId', - projectId, - ), - ) - .body(patchData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch( - projectMemberContract.patchMembers.path.replace( - ':projectId', - projectId, - ), - ) - .body(patchData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - }); - - describe('removeMember', () => { - it('should remove member for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessRemoveMemberMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .delete( - projectMemberContract.removeMember.path - .replace(':projectId', projectId) - .replace(':userId', userId), - ) - .end(); - - expect(response.json()).toEqual([]); - expect(response.statusCode).toEqual(200); - }); - - it('should be able leave a project', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessRemoveMemberMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .delete( - projectMemberContract.removeMember.path - .replace(':projectId', projectId) - .replace(':userId', userId), - ) - .end(); - - expect(response.json()).toEqual([]); - expect(response.statusCode).toEqual(200); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - projectMemberContract.removeMember.path - .replace(':projectId', projectId) - .replace(':userId', userId), - ) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - projectMemberContract.removeMember.path - .replace(':projectId', projectId) - .replace(':userId', userId), - ) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - projectMemberContract.removeMember.path - .replace(':projectId', projectId) - .replace(':userId', userId), - ) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - projectMemberContract.removeMember.path - .replace(':projectId', projectId) - .replace(':userId', userId), - ) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + const projectId = faker.string.uuid() + const userId = faker.string.uuid() + + describe('listMembers', () => { + it('should return members for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessListMembersMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectMemberContract.listMembers.path.replace(':projectId', projectId)) + .end() + + expect(businessListMembersMock).toHaveBeenCalledWith(projectId) + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectMemberContract.listMembers.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + }) + + describe('addMember', () => { + const memberData: Partial = { + userId: faker.string.uuid(), + } + + it('should add member for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + const newMember = { + ...memberData, + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + roleIds: [], + } + + businessAddMemberMock.mockResolvedValueOnce([newMember]) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.json()).toEqual([newMember]) + expect(response.statusCode).toEqual(201) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + businessAddMemberMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) + .body(memberData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) + + describe('patchMembers', () => { + const patchData = [{ userId: faker.string.uuid(), roles: [] }] + + it('should patch members for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessPatchMembersMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) + .body(patchData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) + + describe('removeMember', () => { + it('should remove member for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessRemoveMemberMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should be able leave a project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessRemoveMemberMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts index 4d086b1c0..6ba44b71f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts @@ -1,131 +1,82 @@ +import { AdminAuthorized, ProjectAuthorized, projectMemberContract } from '@cpn-console/shared' import { - AdminAuthorized, - ProjectAuthorized, - projectMemberContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - NotFound404, - Unauthorized401, -} from '@old-server/utils/errors'; - -import { - addMember, - listMembers, - patchMembers, - removeMember, -} from './business'; - -@Injectable() -export class ProjectMemberRouterService { - constructor(private readonly appService: AppService) {} - - projectMemberRouter() { - return this.appService.serverInstance.router(projectMemberContract, { - listMembers: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - - const body = await listMembers(projectId); - - return { - status: 200, - body, - }; - }, - - addMember: async ({ request: req, params, body }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if (!ProjectAuthorized.ManageMembers(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await addMember( - projectId, - body, - perms.user.id, - req.id, - perms.projectOwnerId, - ); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 201, - body: resBody, - }; - }, - - patchMembers: async ({ request: req, params, body }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageMembers(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await patchMembers(projectId, body); - - return { - status: 200, - body: resBody, - }; - }, - - removeMember: async ({ request: req, params }) => { - const { projectId, userId } = params; - const perms = await authUser(req, { id: projectId }); - - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - - if ( - !ProjectAuthorized.ManageMembers(perms) && - userId !== perms.user?.id - ) - return new Forbidden403(); - - const resBody = await removeMember(projectId, params.userId); - - return { - status: 200, - body: resBody, - }; - }, - }); - } + addMember, + listMembers, + patchMembers, + removeMember, +} from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' + +export function projectMemberRouter() { + return serverInstance.router(projectMemberContract, { + listMembers: async ({ request: req, params }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + const body = await listMembers(projectId) + + return { + status: 200, + body, + } + }, + + addMember: async ({ request: req, params, body }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await addMember(projectId, body, perms.user.id, req.id, perms.projectOwnerId) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 201, + body: resBody, + } + }, + + patchMembers: async ({ request: req, params, body }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await patchMembers(projectId, body) + + return { + status: 200, + body: resBody, + } + }, + + removeMember: async ({ request: req, params }) => { + const { projectId, userId } = params + const perms = await authUser(req, { id: projectId }) + + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + if (!ProjectAuthorized.ManageMembers(perms) && userId !== perms.user?.id) return new Forbidden403() + + const resBody = await removeMember(projectId, params.userId) + + return { + status: 200, + body: resBody, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts index 436b9180c..e78bdd54b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts @@ -1,236 +1,195 @@ -import { faker } from '@faker-js/faker'; -import type { ProjectMembers, ProjectRole, User } from '@prisma/client'; -import { describe, expect, it } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import { BadRequest400 } from '../../utils/errors'; -import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from './business'; - -const projectId = faker.string.uuid(); +import { faker } from '@faker-js/faker' +import { describe, expect, it } from 'vitest' +import type { ProjectMembers, ProjectRole, User } from '@prisma/client' +import prisma from '../../__mocks__/prisma' +import { BadRequest400 } from '../../utils/errors' +import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business' + +const projectId = faker.string.uuid() describe('test project-role business', () => { - describe('listRoles', () => { - it('should stringify bigint', async () => { - const partialRole: Partial = { - permissions: 4n, - }; - - prisma.projectRole.findMany.mockResolvedValueOnce([partialRole]); - const response = await listRoles(projectId); - expect(response).toEqual([{ permissions: '4' }]); - }); - }); - - describe('createRole', () => { - it('should create role with incremented position when position 0 is the highest', async () => { - const dbRole: Partial = { - projectId, - permissions: 4n, - position: 0, - }; - - prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole); - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]); - prisma.projectRole.create.mockResolvedValue(null); - await createRole(projectId, { name: 'test', permissions: '4' }); - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ - data: { name: 'test', permissions: 4n, position: 1, projectId }, - }); - }); - - it('should create role with incremented position with bigger position', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - }; - - prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole); - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]); - prisma.projectRole.create.mockResolvedValue(null); - await createRole(projectId, { name: 'test', permissions: '4' }); - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ - data: { - name: 'test', - permissions: 4n, - position: 51, - projectId, - }, - }); - }); - - it('should create role with incremented position with no role in db', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - }; - - prisma.projectRole.findFirst.mockResolvedValueOnce(undefined); - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]); - prisma.projectRole.create.mockResolvedValue(null); - await createRole(projectId, { name: 'test', permissions: '4' }); - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ - data: { name: 'test', permissions: 4n, position: 0, projectId }, - }); - }); - }); - - describe('deleteRole', () => { - const roleId = faker.string.uuid(); - it('should delete role and remove id from concerned users', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - id: faker.string.uuid(), - }; - const members = [ - { - userId: faker.string.uuid(), - projectId, - roleIds: [roleId], - }, - { - userId: faker.string.uuid(), - projectId, - roleIds: [roleId, faker.string.uuid()], - }, - ] as const satisfies Partial[]; - - prisma.projectMembers.findMany.mockResolvedValueOnce(members); - prisma.projectRole.findMany.mockResolvedValueOnce([]); - prisma.projectRole.delete.mockResolvedValue(dbRole); - await deleteRole(roleId); - - expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(1, { - where: expect.any(Object), - data: { roleIds: { set: [] } }, - }); - expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(2, { - where: expect.any(Object), - data: { roleIds: { set: [members[1].roleIds[1]] } }, - }); - expect(prisma.projectRole.delete).toHaveBeenCalledWith({ - where: { id: roleId }, - }); - }); - }); - describe.skip('countRolesMembers', () => { - it('should return aggregated role member counts', async () => { - const partialRoles = [ - { - id: faker.string.uuid(), - }, - { - id: faker.string.uuid(), - }, - ] as const satisfies Partial[]; - - const users = [ - { - projectRoleIds: [partialRoles[0].id, partialRoles[1].id], - }, - { - projectRoleIds: [partialRoles[1].id], - }, - ] as const satisfies Partial[]; - prisma.projectRole.findMany.mockResolvedValue(partialRoles); - prisma.user.findMany.mockResolvedValue(users); - - const response = await countRolesMembers(); - - expect(response).toEqual({ - [partialRoles[0].id]: 1, - [partialRoles[1].id]: 2, - }); - }); - }); - describe('patchRoles', () => { - const dbRoles: ProjectRole[] = [ - { - id: faker.string.uuid(), - name: faker.company.name(), - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 0, - projectId, - }, - { - id: faker.string.uuid(), - name: faker.company.name(), - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 1, - projectId, - }, - ]; - - it('should do nothing', async () => { - prisma.projectRole.findMany.mockResolvedValue([]); - await patchRoles(projectId, []); - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0); - }); - - it('should return 400 if incoherent positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 1 }, - ]; - prisma.projectRole.findMany.mockResolvedValue(dbRoles); - - const response = await patchRoles(projectId, updateRoles); - - expect(response).instanceOf(BadRequest400); - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0); - }); - - it('should return 400 if incoherent positions (missing)', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, position: 1 }, - ]; - prisma.projectRole.findMany.mockResolvedValue(dbRoles); - - const response = await patchRoles(projectId, updateRoles); - - expect(response).instanceOf(BadRequest400); - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0); - }); - - it('should update positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 0 }, - ]; - prisma.projectRole.findMany.mockResolvedValue(dbRoles); - - await patchRoles(projectId, updateRoles); - - expect(prisma.projectRole.update).toHaveBeenCalledTimes(2); - }); - - it('should update permissions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, permissions: '0' }, - ]; - prisma.projectRole.findMany.mockResolvedValue(dbRoles); - - await patchRoles(projectId, updateRoles); - - expect(prisma.projectRole.update).toHaveBeenCalledTimes(1); - expect(prisma.projectRole.update).toHaveBeenCalledWith({ - data: { - name: dbRoles[1].name, - permissions: 0n, - position: 1, - }, - where: { - id: dbRoles[1].id, - }, - }); - }); - }); -}); + describe('listRoles', () => { + it('should stringify bigint', async () => { + const partialRole: Partial = { + permissions: 4n, + } + + prisma.projectRole.findMany.mockResolvedValueOnce([partialRole]) + const response = await listRoles(projectId) + expect(response).toEqual([{ permissions: '4' }]) + }) + }) + + describe('createRole', () => { + it('should create role with incremented position when position 0 is the highest', async () => { + const dbRole: Partial = { + projectId, + permissions: 4n, + position: 0, + } + + prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole) + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.projectRole.create.mockResolvedValue(null) + await createRole(projectId, { name: 'test', permissions: '4' }) + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 1, projectId } }) + }) + + it('should create role with incremented position with bigger position', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + } + + prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole) + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.projectRole.create.mockResolvedValue(null) + await createRole(projectId, { name: 'test', permissions: '4' }) + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 51, projectId } }) + }) + + it('should create role with incremented position with no role in db', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + } + + prisma.projectRole.findFirst.mockResolvedValueOnce(undefined) + prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) + prisma.projectRole.create.mockResolvedValue(null) + await createRole(projectId, { name: 'test', permissions: '4' }) + + expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 0, projectId } }) + }) + }) + + describe('deleteRole', () => { + const roleId = faker.string.uuid() + it('should delete role and remove id from concerned users', async () => { + const dbRole: Partial = { + permissions: 4n, + position: 50, + id: faker.string.uuid(), + } + const members = [{ + userId: faker.string.uuid(), + projectId, + roleIds: [roleId], + }, { + userId: faker.string.uuid(), + projectId, + roleIds: [roleId, faker.string.uuid()], + }] as const satisfies Partial[] + + prisma.projectMembers.findMany.mockResolvedValueOnce(members) + prisma.projectRole.findMany.mockResolvedValueOnce([]) + prisma.projectRole.delete.mockResolvedValue(dbRole) + await deleteRole(roleId) + + expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(1, { where: expect.any(Object), data: { roleIds: { set: [] } } }) + expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(2, { where: expect.any(Object), data: { roleIds: { set: [members[1].roleIds[1]] } } }) + expect(prisma.projectRole.delete).toHaveBeenCalledWith({ where: { id: roleId } }) + }) + }) + describe.skip('countRolesMembers', () => { + it('should return aggregated role member counts', async () => { + const partialRoles = [{ + id: faker.string.uuid(), + }, { + id: faker.string.uuid(), + }] as const satisfies Partial[] + + const users = [{ + projectRoleIds: [partialRoles[0].id, partialRoles[1].id], + }, { + projectRoleIds: [partialRoles[1].id], + }] as const satisfies Partial[] + prisma.projectRole.findMany.mockResolvedValue(partialRoles) + prisma.user.findMany.mockResolvedValue(users) + + const response = await countRolesMembers() + + expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 }) + }) + }) + describe('patchRoles', () => { + const dbRoles: ProjectRole[] = [{ + id: faker.string.uuid(), + name: faker.company.name(), + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 0, + projectId, + }, { + id: faker.string.uuid(), + name: faker.company.name(), + permissions: faker.number.bigInt({ min: 0n, max: 50000n }), + position: 1, + projectId, + }] + + it('should do nothing', async () => { + prisma.projectRole.findMany.mockResolvedValue([]) + await patchRoles(projectId, []) + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) + }) + + it('should return 400 if incoherent positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 1 }, + ] + prisma.projectRole.findMany.mockResolvedValue(dbRoles) + + const response = await patchRoles(projectId, updateRoles) + + expect(response).instanceOf(BadRequest400) + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) + }) + + it('should return 400 if incoherent positions (missing)', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, position: 1 }, + ] + prisma.projectRole.findMany.mockResolvedValue(dbRoles) + + const response = await patchRoles(projectId, updateRoles) + + expect(response).instanceOf(BadRequest400) + expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) + }) + + it('should update positions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[0].id, position: 1 }, + { id: dbRoles[1].id, position: 0 }, + ] + prisma.projectRole.findMany.mockResolvedValue(dbRoles) + + await patchRoles(projectId, updateRoles) + + expect(prisma.projectRole.update).toHaveBeenCalledTimes(2) + }) + + it('should update permissions', async () => { + const updateRoles: Pick = [ + { id: dbRoles[1].id, permissions: '0' }, + ] + prisma.projectRole.findMany.mockResolvedValue(dbRoles) + + await patchRoles(projectId, updateRoles) + + expect(prisma.projectRole.update).toHaveBeenCalledTimes(1) + expect(prisma.projectRole.update).toHaveBeenCalledWith({ + data: { + name: dbRoles[1].name, + permissions: 0n, + position: 1, + }, + where: { + id: dbRoles[1].id, + }, + }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts index f7e7efaf9..2bae2b7ed 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts @@ -1,103 +1,77 @@ -import type { projectRoleContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; +import type { projectRoleContract } from '@cpn-console/shared' +import type { Project, ProjectRole } from '@prisma/client' import { - deleteRole as deleteRoleQuery, - listMembers, - listRoles as listRolesQuery, - updateRole, -} from '@old-server/resources/queries-index'; -import { BadRequest400 } from '@old-server/utils/errors'; -import type { Project, ProjectRole } from '@prisma/client'; + deleteRole as deleteRoleQuery, + listMembers, + listRoles as listRolesQuery, + updateRole, +} from '@old-server/resources/queries-index' +import { BadRequest400 } from '@old-server/utils/errors' +import prisma from '@old-server/prisma' export async function listRoles(projectId: Project['id']) { - return listRolesQuery(projectId).then((roles) => - roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - ); + return listRolesQuery(projectId) + .then(roles => roles.map(role => ({ ...role, permissions: role.permissions.toString() }))) } -export async function patchRoles( - projectId: Project['id'], - roles: typeof projectRoleContract.patchProjectRoles.body._type, -) { - const dbRoles = await listRoles(projectId); - const positionsAvailable: number[] = []; +export async function patchRoles(projectId: Project['id'], roles: typeof projectRoleContract.patchProjectRoles.body._type) { + const dbRoles = await listRoles(projectId) + const positionsAvailable: number[] = [] - const updatedRoles = dbRoles - .filter((dbRole) => roles.find((role) => role.id === dbRole.id)) // filter non concerned dbRoles - .map((dbRole) => { - const matchingRole = roles.find((role) => role.id === dbRole.id); - if ( - typeof matchingRole?.position !== 'undefined' && - !positionsAvailable.includes(matchingRole.position) - ) { - positionsAvailable.push(matchingRole.position); - } - return { - id: matchingRole?.id ?? dbRole.id, - name: matchingRole?.name ?? dbRole.name, - permissions: matchingRole?.permissions - ? BigInt(matchingRole?.permissions) - : BigInt(dbRole.permissions), - position: matchingRole?.position ?? dbRole.position, - }; - }); - if ( - positionsAvailable.length && - positionsAvailable.length !== dbRoles.length - ) - return new BadRequest400( - 'Les numéros de position des rôles sont incohérentes', - ); - for (const { id, ...role } of updatedRoles) { - await updateRole(id, role); - } + const updatedRoles = dbRoles + .filter(dbRole => roles.find(role => role.id === dbRole.id)) // filter non concerned dbRoles + .map((dbRole) => { + const matchingRole = roles.find(role => role.id === dbRole.id) + if (typeof matchingRole?.position !== 'undefined' && !positionsAvailable.includes(matchingRole.position)) { + positionsAvailable.push(matchingRole.position) + } + return { + id: matchingRole?.id ?? dbRole.id, + name: matchingRole?.name ?? dbRole.name, + permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : BigInt(dbRole.permissions), + position: matchingRole?.position ?? dbRole.position, + } + }) + if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes') + for (const { id, ...role } of updatedRoles) { + await updateRole(id, role) + } - return listRoles(projectId); + return listRoles(projectId) } -export async function createRole( - projectId: Project['id'], - role: typeof projectRoleContract.createProjectRole.body._type, -) { - const dbMaxPosRole = - ( - await prisma.projectRole.findFirst({ - where: { projectId }, - orderBy: { position: 'desc' }, - select: { position: true }, - }) - )?.position ?? -1; +export async function createRole(projectId: Project['id'], role: typeof projectRoleContract.createProjectRole.body._type) { + const dbMaxPosRole = (await prisma.projectRole.findFirst({ + where: { projectId }, + orderBy: { position: 'desc' }, + select: { position: true }, + }))?.position ?? -1 - await prisma.projectRole.create({ - data: { - ...role, - projectId, - position: dbMaxPosRole + 1, - permissions: BigInt(role.permissions), - }, - }); + await prisma.projectRole.create({ + data: { + ...role, + projectId, + position: dbMaxPosRole + 1, + permissions: BigInt(role.permissions), + }, + }) - return listRoles(projectId); + return listRoles(projectId) } export async function countRolesMembers(projectId: Project['id']) { - const roles = await listRoles(projectId); - const members = await listMembers(projectId); - const rolesCounts: Record = Object.fromEntries( - roles.map((role) => [role.id, 0]), - ); // {role uuid: 0} - for (const { roleIds } of members) { - for (const roleId of roleIds) { - rolesCounts[roleId]++; - } + const roles = await listRoles(projectId) + const members = await listMembers(projectId) + const rolesCounts: Record = Object.fromEntries(roles.map(role => [role.id, 0])) // {role uuid: 0} + for (const { roleIds } of members) { + for (const roleId of roleIds) { + rolesCounts[roleId]++ } - return rolesCounts; + } + return rolesCounts } export async function deleteRole(roleId: Project['id']) { - await deleteRoleQuery(roleId); - return null; + await deleteRoleQuery(roleId) + return null } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts index a3f575921..08c374294 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts @@ -1,63 +1,54 @@ -import prisma from '@old-server/prisma'; -import type { Prisma, Project, ProjectRole } from '@prisma/client'; +import type { + Prisma, + Project, -export const listRoles = (projectId: Project['id']) => - prisma.projectRole.findMany({ - where: { projectId }, - orderBy: { position: 'asc' }, - }); + ProjectRole, +} from '@prisma/client' -export function createRole( - data: Pick< - Prisma.ProjectRoleUncheckedCreateInput, - 'permissions' | 'name' | 'position' | 'projectId' - >, -) { - return prisma.projectRole.create({ - data: { - name: data.name, - permissions: 0n, - position: data.position, - projectId: data.projectId, - }, - }); +import prisma from '@old-server/prisma' + +export const listRoles = (projectId: Project['id']) => prisma.projectRole.findMany({ where: { projectId }, orderBy: { position: 'asc' } }) + +export function createRole(data: Pick) { + return prisma.projectRole.create({ + data: { + name: data.name, + permissions: 0n, + position: data.position, + projectId: data.projectId, + }, + }) } -export function updateRole( - id: ProjectRole['id'], - data: Pick< - Prisma.ProjectRoleUncheckedUpdateInput, - 'permissions' | 'name' | 'position' | 'id' - >, -) { - return prisma.projectRole.update({ - where: { id }, - data, - }); +export function updateRole(id: ProjectRole['id'], data: Pick) { + return prisma.projectRole.update({ + where: { id }, + data, + }) } export async function deleteRole(id: ProjectRole['id']) { - const role = await prisma.projectRole.delete({ - where: { - id, + const role = await prisma.projectRole.delete({ + where: { + id, + }, + }) + const attachedMembers = await prisma.projectMembers.findMany({ + where: { projectId: role.projectId, roleIds: { has: id } }, + }) + for (const member of attachedMembers) { + await prisma.projectMembers.update({ + where: { + projectId_userId: { + projectId: role.projectId, + userId: member.userId, + }, + }, + data: { + roleIds: { + set: member.roleIds.filter(roleId => roleId !== id), }, - }); - const attachedMembers = await prisma.projectMembers.findMany({ - where: { projectId: role.projectId, roleIds: { has: id } }, - }); - for (const member of attachedMembers) { - await prisma.projectMembers.update({ - where: { - projectId_userId: { - projectId: role.projectId, - userId: member.userId, - }, - }, - data: { - roleIds: { - set: member.roleIds.filter((roleId) => roleId !== id), - }, - }, - }); - } + }, + }) + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts index 299e04ca0..ccdb1cb4b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts @@ -1,495 +1,316 @@ -import { PROJECT_PERMS, projectRoleContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock('./business'); -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); - -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessCreateRoleMock = vi.spyOn(business, 'createRole'); -const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole'); -const businessListRolesMock = vi.spyOn(business, 'listRoles'); -const businessPatchRolesMock = vi.spyOn(business, 'patchRoles'); -const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers'); +import { faker } from '@faker-js/faker' +import { PROJECT_PERMS, projectRoleContract } from '@cpn-console/shared' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' +import { BadRequest400 } from '../../utils/errors' +import * as business from './business' + +vi.mock('./business') +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) + +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessCreateRoleMock = vi.spyOn(business, 'createRole') +const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole') +const businessListRolesMock = vi.spyOn(business, 'listRoles') +const businessPatchRolesMock = vi.spyOn(business, 'patchRoles') +const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers') describe('tests projectRoleContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - const projectId = faker.string.uuid(); - const roleId = faker.string.uuid(); - - describe('listProjectRoles', () => { - it('should return roles for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.SEE_SECRETS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessListRolesMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .get( - projectRoleContract.listProjectRoles.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(200); - expect(response.json()).toEqual([]); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - projectRoleContract.listProjectRoles.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(404); - }); - }); - - describe('createProjectRole', () => { - it('should create role for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCreateRoleMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .post( - projectRoleContract.createProjectRole.path.replace( - ':projectId', - projectId, - ), - ) - .body({ name: 'nouveau rôle' }) - .end(); - - expect(response.json()).toEqual([]); - expect(response.statusCode).toEqual(201); - }); - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectRoleContract.createProjectRole.path.replace( - ':projectId', - projectId, - ), - ) - .body({ name: 'nouveau rôle' }) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.SEE_SECRETS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectRoleContract.createProjectRole.path.replace( - ':projectId', - projectId, - ), - ) - .body({ name: 'nouveau rôle' }) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectRoleContract.createProjectRole.path.replace( - ':projectId', - projectId, - ), - ) - .body({ name: 'nouveau rôle' }) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectRoleContract.createProjectRole.path.replace( - ':projectId', - projectId, - ), - ) - .body({ name: 'nouveau rôle' }) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - }); - - describe('patchProjectRoles', () => { - it('should patch roles for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessPatchRolesMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .patch( - projectRoleContract.patchProjectRoles.path.replace( - ':projectId', - projectId, - ), - ) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end(); - - expect(response.json()).toEqual([]); - expect(response.statusCode).toEqual(200); - }); - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch( - projectRoleContract.patchProjectRoles.path.replace( - ':projectId', - projectId, - ), - ) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.SEE_SECRETS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch( - projectRoleContract.patchProjectRoles.path.replace( - ':projectId', - projectId, - ), - ) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch( - projectRoleContract.patchProjectRoles.path.replace( - ':projectId', - projectId, - ), - ) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch( - projectRoleContract.patchProjectRoles.path.replace( - ':projectId', - projectId, - ), - ) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessPatchRolesMock.mockResolvedValue( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .patch( - projectRoleContract.patchProjectRoles.path.replace( - ':projectId', - projectId, - ), - ) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end(); - - expect(response.statusCode).toEqual(400); - }); - }); - - describe('projectRoleMemberCounts', () => { - it('should return member counts for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.SEE_SECRETS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCountRolesMembersMock.mockResolvedValueOnce({}); - - const response = await app - .inject() - .get( - projectRoleContract.projectRoleMemberCounts.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(200); - expect(response.json()).toEqual({}); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - projectRoleContract.projectRoleMemberCounts.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(404); - }); - }); - - describe('deleteProjectRole', () => { - it('should delete role for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteRoleMock.mockResolvedValueOnce(null); - const response = await app - .inject() - .delete( - projectRoleContract.deleteProjectRole.path - .replace(':projectId', projectId) - .replace(':roleId', roleId), - ) - .end(); - - expect(response.statusCode).toEqual(204); - }); - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCreateRoleMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .delete( - projectRoleContract.deleteProjectRole.path - .replace(':projectId', projectId) - .replace(':roleId', roleId), - ) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.SEE_SECRETS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCreateRoleMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .delete( - projectRoleContract.deleteProjectRole.path - .replace(':projectId', projectId) - .replace(':roleId', roleId), - ) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCreateRoleMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .delete( - projectRoleContract.deleteProjectRole.path - .replace(':projectId', projectId) - .replace(':roleId', roleId), - ) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_ROLES, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCreateRoleMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .delete( - projectRoleContract.deleteProjectRole.path - .replace(':projectId', projectId) - .replace(':roleId', roleId), - ) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + const projectId = faker.string.uuid() + const roleId = faker.string.uuid() + + describe('listProjectRoles', () => { + it('should return roles for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessListRolesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectRoleContract.listProjectRoles.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectRoleContract.listProjectRoles.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + }) + + describe('createProjectRole', () => { + it('should create role for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(201) + }) + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) + .body({ name: 'nouveau rôle' }) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) + + describe('patchProjectRoles', () => { + it('should patch roles for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessPatchRolesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessPatchRolesMock.mockResolvedValue(new BadRequest400('une erreur')) + const response = await app.inject() + .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) + .body([{ id: roleId, name: 'nouveau rôle' }]) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('projectRoleMemberCounts', () => { + it('should return member counts for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCountRolesMembersMock.mockResolvedValueOnce({}) + + const response = await app.inject() + .get(projectRoleContract.projectRoleMemberCounts.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual({}) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectRoleContract.projectRoleMemberCounts.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + }) + + describe('deleteProjectRole', () => { + it('should delete role for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteRoleMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 for locked project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not permited', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateRoleMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts index 230f7735e..44a7e4e1a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts @@ -1,137 +1,90 @@ +import { AdminAuthorized, ProjectAuthorized, projectRoleContract } from '@cpn-console/shared' import { - AdminAuthorized, - ProjectAuthorized, - projectRoleContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - NotFound404, -} from '@old-server/utils/errors'; - -import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from './business'; - -@Injectable() -export class ProjectRoleRouterService { - constructor(private readonly appService: AppService) {} - - projectRoleRouter() { - return this.appService.serverInstance.router(projectRoleContract, { - // Récupérer des projets - listProjectRoles: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - - const body = await listRoles(projectId); - - return { - status: 200, - body, - }; - }, - - createProjectRole: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await createRole(projectId, body); - - return { - status: 201, - body: resBody, - }; - }, - - patchProjectRoles: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await patchRoles(projectId, body); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - projectRoleMemberCounts: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - - const resBody = await countRolesMembers(projectId); - - return { - status: 200, - body: resBody, - }; - }, - - deleteProjectRole: async ({ - request: req, - params: { projectId, roleId }, - }) => { - const perms = await authUser(req, { id: projectId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await deleteRole(roleId); - - return { - status: 204, - body: resBody, - }; - }, - }); - } + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403, NotFound404 } from '@old-server/utils/errors' + +export function projectRoleRouter() { + return serverInstance.router(projectRoleContract, { + // Récupérer des projets + listProjectRoles: async ({ request: req, params }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + const body = await listRoles(projectId) + + return { + status: 200, + body, + } + }, + + createProjectRole: async ({ request: req, params: { projectId }, body }) => { + const perms = await authUser(req, { id: projectId }) + + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await createRole(projectId, body) + + return { + status: 201, + body: resBody, + } + }, + + patchProjectRoles: async ({ request: req, params: { projectId }, body }) => { + const perms = await authUser(req, { id: projectId }) + + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await patchRoles(projectId, body) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 200, + body: resBody, + } + }, + + projectRoleMemberCounts: async ({ request: req, params }) => { + const { projectId } = params + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + const resBody = await countRolesMembers(projectId) + + return { + status: 200, + body: resBody, + } + }, + + deleteProjectRole: async ({ request: req, params: { projectId, roleId } }) => { + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const resBody = await deleteRole(roleId) + + return { + status: 204, + body: resBody, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts index 6eeaa1354..531e3ba12 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts @@ -1,124 +1,95 @@ -import { - editStrippers, - populatePluginManifests, - servicesInfos, -} from '@cpn-console/hooks'; -import type { ZoneObject } from '@cpn-console/hooks'; +import type { Project, ProjectPlugin } from '@prisma/client' import type { - PermissionTarget, - PluginsUpdateBody, - ServiceUrl, -} from '@cpn-console/shared'; + PermissionTarget, + PluginsUpdateBody, + ServiceUrl, +} from '@cpn-console/shared' +import { editStrippers, populatePluginManifests, servicesInfos } from '@cpn-console/hooks' +import type { ZoneObject } from '@cpn-console/hooks' import { - getAdminPlugin, - getProjectInfosByIdOrThrow, - getProjectStore, - getPublicClusters, - saveProjectStore, -} from '@old-server/resources/queries-index'; -import type { Project, ProjectPlugin } from '@prisma/client'; + getAdminPlugin, + getProjectInfosByIdOrThrow, + getProjectStore, + getPublicClusters, + saveProjectStore, +} from '@old-server/resources/queries-index' export type ConfigRecords = { - key: string; - pluginName: string; - value: string | number | null; -}[]; + key: string + pluginName: string + value: string | number | null +}[] -export function dbToObj( - records: Omit[], -): PluginsUpdateBody { - const obj: PluginsUpdateBody = {}; - for (const record of records) { - if (!obj[record.pluginName]) obj[record.pluginName] = {}; - obj[record.pluginName][record.key] = record.value; - } - return obj; +export function dbToObj(records: Omit[]): PluginsUpdateBody { + const obj: PluginsUpdateBody = {} + for (const record of records) { + if (!obj[record.pluginName]) obj[record.pluginName] = {} + obj[record.pluginName][record.key] = record.value + } + return obj } export function objToDb(obj: PluginsUpdateBody): ConfigRecords { - return Object.entries(obj) - .map(([pluginName, values]) => - Object.entries(values).map(([key, value]) => ({ - pluginName, - key, - value, - })), - ) - .flat(); + return Object.entries(obj) + .map(([pluginName, values]) => Object.entries(values) + .map(([key, value]) => ({ pluginName, key, value }))) + .flat() } -export async function getProjectServices( - projectId: Project['id'], - permissionTarget: PermissionTarget, -) { - // Pré-requis - const project = await getProjectInfosByIdOrThrow(projectId); +export async function getProjectServices(projectId: Project['id'], permissionTarget: PermissionTarget) { + // Pré-requis + const project = await getProjectInfosByIdOrThrow(projectId) - const [projectStore, globalConfig] = await Promise.all([ - getProjectStore(projectId), - getAdminPlugin(), - ]); - const store = dbToObj([...projectStore, ...globalConfig]); + const [projectStore, globalConfig] = await Promise.all([ + getProjectStore(projectId), + getAdminPlugin(), + ]) + const store = dbToObj([...projectStore, ...globalConfig]) - const publicClusters = await getPublicClusters(); - project.clusters = project.clusters.concat(publicClusters); - const zones: Map = new Map(); // Pour dédoublonnage des zones - project.clusters.map((c) => zones.set(c.zone.id, c.zone)); + const publicClusters = await getPublicClusters() + project.clusters = project.clusters.concat(publicClusters) + const zones: Map = new Map() // Pour dédoublonnage des zones + project.clusters.map(c => zones.set(c.zone.id, c.zone)) - return Object.values(servicesInfos) - .map(({ name, title, to, imgSrc, description }) => { - let urls: ServiceUrl[] = []; - const toResponse = to - ? to({ - clusters: project.clusters, - zones: Array.from(zones.values()), - environments: project.environments, - project, - store, - }) - : []; - if (Array.isArray(toResponse)) { - urls = toResponse.map((res) => ({ - name: res.title ?? '', - description: res.description ?? '', - to: res.to, - })); - } else if (typeof toResponse === 'string') { - urls = [{ to: toResponse, name: '' }]; - } else if (toResponse) { - urls = [{ name: toResponse.title ?? '', to: toResponse.to }]; - } - const manifest = populatePluginManifests({ - data: { - project: projectStore, - global: globalConfig, - }, - permissionTarget, - pluginName: name, - select: { - global: true, - project: true, - }, - }); - return { imgSrc, title, name, urls, manifest, description }; + return Object.values(servicesInfos).map(({ name, title, to, imgSrc, description }) => { + let urls: ServiceUrl[] = [] + const toResponse = to + ? to({ + clusters: project.clusters, + zones: Array.from(zones.values()), + environments: project.environments, + project, + store, }) - .filter( - (s) => - s.urls.length || - s.manifest.global?.length || - s.manifest.project?.length, - ); + : [] + if (Array.isArray(toResponse)) { + urls = toResponse.map(res => ({ name: res.title ?? '', description: res.description ?? '', to: res.to })) + } else if (typeof toResponse === 'string') { + urls = [{ to: toResponse, name: '' }] + } else if (toResponse) { + urls = [{ name: toResponse.title ?? '', to: toResponse.to }] + } + const manifest = populatePluginManifests({ + data: { + project: projectStore, + global: globalConfig, + }, + permissionTarget, + pluginName: name, + select: { + global: true, + project: true, + }, + }) + return { imgSrc, title, name, urls, manifest, description } + }).filter(s => s.urls.length || s.manifest.global?.length || s.manifest.project?.length) } -export async function updateProjectServices( - projectId: Project['id'], - data: PluginsUpdateBody, - stripperRoles: Array<'user' | 'admin'>, -) { - for (const role of stripperRoles) { - const parsedData = editStrippers.project[role].safeParse(data); - if (!parsedData.success) continue; - await saveProjectStore(objToDb(parsedData.data), projectId); - } - return null; +export async function updateProjectServices(projectId: Project['id'], data: PluginsUpdateBody, stripperRoles: Array<'user' | 'admin'>) { + for (const role of stripperRoles) { + const parsedData = editStrippers.project[role].safeParse(data) + if (!parsedData.success) continue + await saveProjectStore(objToDb(parsedData.data), projectId) + } + return null } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts index 0bceefba1..a966eb59a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts @@ -1,58 +1,54 @@ -import prisma from '@old-server/prisma'; -import type { Project } from '@prisma/client'; - -import type { ConfigRecords } from './business'; +import type { Project } from '@prisma/client' +import type { ConfigRecords } from './business' +import prisma from '@old-server/prisma' // CONFIG export function getProjectStore(projectId: Project['id']) { - return prisma.projectPlugin.findMany({ - where: { projectId }, - select: { - key: true, - pluginName: true, - value: true, - }, - }); + return prisma.projectPlugin.findMany({ + where: { projectId }, + select: { + key: true, + pluginName: true, + value: true, + }, + }) } -export const getAdminPlugin = prisma.adminPlugin.findMany; +export const getAdminPlugin = prisma.adminPlugin.findMany -export async function saveProjectStore( - records: ConfigRecords, - projectId: Project['id'], -) { - for (const { pluginName, key, value } of records) { - if (value === null) { - await prisma.projectPlugin.delete({ - where: { - projectId_pluginName_key: { - projectId, - pluginName, - key, - }, - }, - }); - } else { - await prisma.projectPlugin.upsert({ - create: { - pluginName, - projectId, - key, - value: value.toString(), - }, - update: { - key, - value: value.toString(), - pluginName, - }, - where: { - projectId_pluginName_key: { - projectId, - pluginName, - key, - }, - }, - }); - } +export async function saveProjectStore(records: ConfigRecords, projectId: Project['id']) { + for (const { pluginName, key, value } of records) { + if (value === null) { + await prisma.projectPlugin.delete({ + where: { + projectId_pluginName_key: { + projectId, + pluginName, + key, + }, + }, + }) + } else { + await prisma.projectPlugin.upsert({ + create: { + pluginName, + projectId, + key, + value: value.toString(), + }, + update: { + key, + value: value.toString(), + pluginName, + }, + where: { + projectId_pluginName_key: { + projectId, + pluginName, + key, + }, + }, + }) } + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts index eb2819915..2e1972238 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts @@ -1,256 +1,160 @@ -import { PROJECT_PERMS, projectServiceContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessGetServicesMock = vi.spyOn(business, 'getProjectServices'); -const businessUpdateServicesMock = vi.spyOn(business, 'updateProjectServices'); +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PROJECT_PERMS, projectServiceContract } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetServicesMock = vi.spyOn(business, 'getProjectServices') +const businessUpdateServicesMock = vi.spyOn(business, 'updateProjectServices') describe('projectServiceRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - const projectId = faker.string.uuid(); - - describe('getServices', () => { - it('should return services for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessGetServicesMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .get( - projectServiceContract.getServices.path.replace( - ':projectId', - projectId, - ), - ) - .query({ permissionTarget: 'user' }) - .end(); - - expect(businessGetServicesMock).toHaveBeenCalledWith( - projectId, - 'user', - ); - expect(response.statusCode).toEqual(200); - expect(response.json()).toEqual([]); - }); - - it('should not return admin services for non admin', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessGetServicesMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .get( - projectServiceContract.getServices.path.replace( - ':projectId', - projectId, - ), - ) - .query({ permissionTarget: 'admin' }) - .end(); - - expect(businessGetServicesMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - - it('should return services for admin', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.GUEST, - }); - const user = getUserMockInfos(true, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessGetServicesMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .get( - projectServiceContract.getServices.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(businessGetServicesMock).toHaveBeenCalledWith( - projectId, - 'user', - ); - expect(response.statusCode).toEqual(200); - expect(response.json()).toEqual([]); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - projectServiceContract.getServices.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(404); - }); - }); - - describe('updateProjectServices', () => { - const updateData = { serviceA: { param1: 'value' } }; - - it('should update services for project manager', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateServicesMock.mockResolvedValueOnce(null); - - const response = await app - .inject() - .post( - projectServiceContract.updateProjectServices.path.replace( - ':projectId', - projectId, - ), - ) - .body(updateData) - .end(); - - expect(businessUpdateServicesMock).toHaveBeenCalledWith( - projectId, - updateData, - ['user'], - ); - expect(response.statusCode).toEqual(204); - }); - - it('should update services for project admin', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE, - }); - const user = getUserMockInfos(true, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateServicesMock.mockResolvedValueOnce(null); - - const response = await app - .inject() - .post( - projectServiceContract.updateProjectServices.path.replace( - ':projectId', - projectId, - ), - ) - .body(updateData) - .end(); - - expect(businessUpdateServicesMock).toHaveBeenCalledWith( - projectId, - updateData, - ['user', 'admin'], - ); - expect(response.statusCode).toEqual(204); - }); - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectServiceContract.updateProjectServices.path.replace( - ':projectId', - projectId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectServiceContract.updateProjectServices.path.replace( - ':projectId', - projectId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - projectServiceContract.updateProjectServices.path.replace( - ':projectId', - projectId, - ), - ) - .body(updateData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + const projectId = faker.string.uuid() + + describe('getServices', () => { + it('should return services for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetServicesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) + .query({ permissionTarget: 'user' }) + .end() + + expect(businessGetServicesMock).toHaveBeenCalledWith(projectId, 'user') + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should not return admin services for non admin', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetServicesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) + .query({ permissionTarget: 'admin' }) + .end() + + expect(businessGetServicesMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + + it('should return services for admin', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetServicesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) + .end() + + expect(businessGetServicesMock).toHaveBeenCalledWith(projectId, 'user') + expect(response.statusCode).toEqual(200) + expect(response.json()).toEqual([]) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + }) + + describe('updateProjectServices', () => { + const updateData = { serviceA: { param1: 'value' } } + + it('should update services for project manager', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateServicesMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(businessUpdateServicesMock).toHaveBeenCalledWith(projectId, updateData, ['user']) + expect(response.statusCode).toEqual(204) + }) + + it('should update services for project admin', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateServicesMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(businessUpdateServicesMock).toHaveBeenCalledWith(projectId, updateData, ['user', 'admin']) + expect(response.statusCode).toEqual(204) + }) + + it('should return 404 for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) + .body(updateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts index 285e204db..a02f6cbe3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts @@ -1,79 +1,38 @@ -import { - AdminAuthorized, - ProjectAuthorized, - projectServiceContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403, NotFound404 } from '@old-server/utils/errors'; - -import { getProjectServices, updateProjectServices } from './business'; - -@Injectable() -export class ProjectServiceRouterService { - constructor(private readonly appService: AppService) {} - - projectServiceRouter() { - return this.appService.serverInstance.router(projectServiceContract, { - // Récupérer les services d'un projet - getServices: async ({ - request: req, - params: { projectId }, - query, - }) => { - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if ( - !AdminAuthorized.isAdmin(perms.adminPermissions) && - query.permissionTarget === 'admin' - ) - return new Forbidden403( - 'Vous ne pouvez pas demander les paramètres admin', - ); - - const body = await getProjectServices( - projectId, - query.permissionTarget, - ); - - return { - status: 200, - body, - }; - }, - - updateProjectServices: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - if (!ProjectAuthorized.Manage(perms)) return new NotFound404(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - - const allowedRoles: Array<'user' | 'admin'> = - AdminAuthorized.isAdmin(perms.adminPermissions) - ? ['user', 'admin'] - : ['user']; - - const resBody = await updateProjectServices( - projectId, - body, - allowedRoles, - ); - return { - status: 204, - body: resBody, - }; - }, - }); - } +import { AdminAuthorized, ProjectAuthorized, projectServiceContract } from '@cpn-console/shared' +import { getProjectServices, updateProjectServices } from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { Forbidden403, NotFound404 } from '@old-server/utils/errors' + +export function projectServiceRouter() { + return serverInstance.router(projectServiceContract, { + // Récupérer les services d'un projet + getServices: async ({ request: req, params: { projectId }, query }) => { + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!AdminAuthorized.isAdmin(perms.adminPermissions) && query.permissionTarget === 'admin') return new Forbidden403('Vous ne pouvez pas demander les paramètres admin') + + const body = await getProjectServices(projectId, query.permissionTarget) + + return { + status: 200, + body, + } + }, + + updateProjectServices: async ({ request: req, params: { projectId }, body }) => { + const perms = await authUser(req, { id: projectId }) + if (!ProjectAuthorized.Manage(perms)) return new NotFound404() + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + + const allowedRoles: Array<'user' | 'admin'> = AdminAuthorized.isAdmin(perms.adminPermissions) ? ['user', 'admin'] : ['user'] + + const resBody = await updateProjectServices(projectId, body, allowedRoles) + return { + status: 204, + body: resBody, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts index b9cd6c237..10195c221 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts @@ -1,518 +1,361 @@ -import { faker } from '@faker-js/faker'; -import type { - Cluster, - Project, - ProjectMembers, - ProjectRole, - User, -} from '@prisma/client'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import { hook } from '../../__mocks__/utils/hook-wrapper'; +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Cluster, Project, ProjectMembers, ProjectRole, User } from '@prisma/client' +import prisma from '../../__mocks__/prisma' +import { hook } from '../../__mocks__/utils/hook-wrapper' +import { dbToObj } from '../project-service/business' +import * as userBusiness from '../user/business' import { - BadRequest400, - ErrorResType, - Unprocessable422, -} from '../../utils/errors'; -import { dbToObj } from '../project-service/business'; -import * as userBusiness from '../user/business'; -import { - archiveProject, - chunk, - createProject, - generateProjectsData, - generateSlug, - getProjectSecrets, - listProjects, - replayHooks, - updateProject, -} from './business'; + BadRequest400, + ErrorResType, + Unprocessable422, +} from '../../utils/errors' +import { archiveProject, chunk, createProject, generateProjectsData, generateSlug, getProjectSecrets, listProjects, replayHooks, updateProject } from './business' vi.mock('../../utils/hook-wrapper', async () => ({ - hook, -})); + hook, +})) -const logViaSessionMock = vi.spyOn(userBusiness, 'logViaSession'); +const logViaSessionMock = vi.spyOn(userBusiness, 'logViaSession') -const projectId = faker.string.uuid(); +const projectId = faker.string.uuid() const user: User = { - id: faker.string.uuid(), - createdAt: new Date(), - updatedAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - adminRoleIds: [], - type: 'human', - lastLogin: null, -}; + id: faker.string.uuid(), + createdAt: new Date(), + updatedAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + adminRoleIds: [], + type: 'human', + lastLogin: null, +} const project: Project & { - clusters: Pick[]; - members: ProjectMembers[]; - roles: ProjectRole[]; - owner: User; + clusters: Pick[] + members: ProjectMembers[] + roles: ProjectRole[] + owner: User } = { - createdAt: new Date(), - updatedAt: new Date(), - description: '', - everyonePerms: 649n, - id: faker.string.uuid(), - locked: false, - name: faker.string.alphanumeric(8), - status: 'created', - ownerId: faker.string.uuid(), - clusters: [], - roles: [], - members: [], -}; -const reqId = faker.string.uuid(); + createdAt: new Date(), + updatedAt: new Date(), + description: '', + everyonePerms: 649n, + id: faker.string.uuid(), + locked: false, + name: faker.string.alphanumeric(8), + status: 'created', + ownerId: faker.string.uuid(), + clusters: [], + roles: [], + members: [], +} +const reqId = faker.string.uuid() describe('test project business utils', () => { - it('should transform arrow ', async () => { - const result = dbToObj([ - { key: 'test', pluginName: 'test', value: 'test' }, - ]); - expect(result).toEqual({ test: { test: 'test' } }); - }); -}); + it('should transform arrow ', async () => { + const result = dbToObj([{ key: 'test', pluginName: 'test', value: 'test' }]) + expect(result).toEqual({ test: { test: 'test' } }) + }) +}) describe('test project business logic', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - describe('listProjects', () => { - it('should return stringified perms', async () => { - prisma.project.findMany.mockResolvedValue([ - { - everyonePerms: 5n, - clusters: [], - roles: [{ permissions: 28n }], - }, - ]); - const response = await listProjects({}, user.id); - expect(response[0].everyonePerms).toBe('5'); - expect(response[0].roles[0].permissions).toBe('28'); - }); - }); - describe('getProjectSecrets', () => { - const getResultsHook = { + beforeEach(() => { + vi.resetAllMocks() + }) + describe('listProjects', () => { + it('should return stringified perms', async () => { + prisma.project.findMany.mockResolvedValue([{ everyonePerms: 5n, clusters: [], roles: [{ permissions: 28n }] }]) + const response = await listProjects({}, user.id) + expect(response[0].everyonePerms).toBe('5') + expect(response[0].roles[0].permissions).toBe('28') + }) + }) + describe('getProjectSecrets', () => { + const getResultsHook = { + failed: false, + args: {}, + results: { + registry: { + secrets: { + token: 'myToken', + }, + status: { failed: false, - args: {}, - results: { - registry: { - secrets: { - token: 'myToken', - }, - status: { - failed: false, - }, - }, - }, - }; - it('should return transform secret', async () => { - hook.project.getSecrets.mockResolvedValue(getResultsHook); - - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - }); - const response = await getProjectSecrets(projectId); - - // according to src/utils/mocks.ts - expect(JSON.stringify(response)).toContain('myToken'); - }); - - it('should return projects secrets', async () => { - hook.project.getSecrets.mockResolvedValue(getResultsHook); - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - }); - prisma.project.findMany.mockResolvedValue({ id: projectId }); - const response = await getProjectSecrets(projectId); - // according to src/utils/mocks.ts - expect(JSON.stringify(response)).toContain('myToken'); - }); - - it('should return hook error', async () => { - hook.project.getSecrets.mockResolvedValue({ failed: true }); - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - }); - prisma.project.findMany.mockResolvedValue({ id: projectId }); - const response = await getProjectSecrets(projectId); - // according to src/utils/mocks.ts - expect(response).toBeInstanceOf(Unprocessable422); - }); - }); - - describe('createProject', () => { - it('should create project', async () => { - logViaSessionMock.mockResolvedValue({ user }); - - prisma.project.create.mockResolvedValue({ - ...project, - status: 'initializing', - }); - prisma.project.findFirst.mockResolvedValue(undefined); - prisma.project.findMany.mockResolvedValue([]); - hook.project.upsert.mockResolvedValue({ - results: {}, - project: { ...project }, - }); - - const projectRes = await createProject(project, user, reqId); - - expect(projectRes.name).toEqual(project.name); - expect(prisma.project.create).toHaveBeenCalledTimes(1); - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - }); - - it('should return plugins failed', async () => { - logViaSessionMock.mockResolvedValue({ user }); - - prisma.project.create.mockResolvedValue({ - ...project, - status: 'initializing', - }); - prisma.project.findFirst.mockResolvedValue(undefined); - prisma.project.findMany.mockResolvedValue([]); - hook.project.upsert.mockResolvedValue({ - results: { failed: true }, - project: { ...project }, - }); - - const response = await createProject(project, user, reqId); - - expect(prisma.project.create).toHaveBeenCalledTimes(1); - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(response).toBeInstanceOf(Unprocessable422); - }); - }); - describe('updateProject', () => { - const updatedProjet = { - description: faker.lorem.lines(2), - everyonePerms: '5', - }; - const reqId = faker.string.uuid(); - const members: ProjectMembers[] = [ - { - userId: faker.string.uuid(), - projectId: project.id, - roleIds: [], - user: { type: 'human' }, - }, - ]; - it('should update project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - members, - }); - prisma.project.update.mockResolvedValue(project); - hook.project.upsert.mockResolvedValue({ - results: {}, - project: { ...project }, - }); - - await updateProject( - { ...updatedProjet, ownerId: members[0].userId }, - project.id, - user, - reqId, - ); - - expect(prisma.project.update).toHaveBeenCalledTimes(2); - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - }); - - it('should update nothing', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - members, - }); - prisma.project.update.mockResolvedValue(project); - hook.project.upsert.mockResolvedValue({ - results: {}, - project: { ...project }, - }); - - await updateProject({}, project.id, user, reqId); - - expect(prisma.project.update).toHaveBeenCalledTimes(0); - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - }); - - it('should not update if project archived', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - status: 'archived', - }); - prisma.project.update.mockResolvedValue(project); - hook.project.upsert.mockResolvedValue({ - results: {}, - project: { ...project }, - }); - - const response = await updateProject({}, project.id, user, reqId); - - expect(response).toBeInstanceOf(ErrorResType); - expect(prisma.project.update).toHaveBeenCalledTimes(0); - expect(prisma.log.create).toHaveBeenCalledTimes(0); - expect(hook.project.upsert).toHaveBeenCalledTimes(0); - }); - - it('should not update project, cause missing member', async () => { - hook.project.upsert.mockResolvedValue({ - results: {}, - project: { ...project }, - }); - logViaSessionMock.mockResolvedValue({ user }); - - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - members: [], - }); - - const response = await updateProject( - { ownerId: members[0].userId }, - project.id, - user, - reqId, - ); - - expect(prisma.project.findUniqueOrThrow).toHaveBeenCalledTimes(1); - expect(response).toBeInstanceOf(BadRequest400); - expect(hook.project.upsert).toHaveBeenCalledTimes(0); - expect(prisma.log.update).toHaveBeenCalledTimes(0); - }); - - it('should return plugins failed', async () => { - logViaSessionMock.mockResolvedValue({ user }); - - prisma.project.findUniqueOrThrow.mockResolvedValue({ - status: 'created', - }); - hook.project.upsert.mockResolvedValue({ - results: { failed: true }, - project: { ...project }, - }); - - const response = await updateProject( - updatedProjet, - project.id, - user, - reqId, - ); - - expect(prisma.project.update).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(response).toBeInstanceOf(Unprocessable422); - }); - }); - describe('replayHooks', () => { - const reqId = faker.string.uuid(); - - it('should replay hooks', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - locked: false, - status: 'created', - }); - hook.project.upsert.mockResolvedValue({ - results: { failed: false }, - }); - - await replayHooks(project.id, user, reqId); - - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - }); - - it('should not replay hooks on archived project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - locked: false, - status: 'archived', - }); - hook.project.upsert.mockResolvedValue({ - results: { failed: false }, - }); - - const response = await replayHooks(project.id, user, reqId); - - expect(response).toBeInstanceOf(ErrorResType); - expect(prisma.log.create).toHaveBeenCalledTimes(0); - expect(hook.project.upsert).toHaveBeenCalledTimes(0); - }); - - it('should not replay hooks on locked project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - locked: true, - status: 'created', - }); - hook.project.upsert.mockResolvedValue({ - results: { failed: false }, - }); - - const response = await replayHooks(project.id, user, reqId); - - expect(response).toBeInstanceOf(ErrorResType); - expect(prisma.log.create).toHaveBeenCalledTimes(0); - expect(hook.project.upsert).toHaveBeenCalledTimes(0); - }); - - it('should update nothing and return error', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - locked: false, - status: 'created', - }); - hook.project.upsert.mockResolvedValue({ - results: { failed: true }, - }); - - const response = await replayHooks(project.id, user, reqId); - - expect(prisma.log.create).toHaveBeenCalledTimes(1); - expect(hook.project.upsert).toHaveBeenCalledTimes(1); - expect(response).toBeInstanceOf(Unprocessable422); - }); - }); - - describe('archiveProject', () => { - it('should archive project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - locked: false, - }); - hook.project.delete.mockResolvedValue({ - results: { failed: false }, - project: Promise.resolve({ status: 'archived' }), - }); - const response = await archiveProject(project.id, user, reqId); - expect(response).toBeNull(); - expect(prisma.project.update).toHaveBeenLastCalledWith({ - where: { id: project.id }, - data: { - clusters: { set: [] }, - }, - }); - }); - - it('should not archive a project already archived', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - locked: false, - status: 'archived', - }); - hook.project.delete.mockResolvedValue({ - results: { failed: false }, - project: Promise.resolve({ status: 'archived' }), - }); - const response = await archiveProject(project.id, user, reqId); - expect(response).toBeInstanceOf(ErrorResType); - expect(prisma.project.update).toHaveBeenCalledTimes(0); - }); - - it('should not archive a project locked', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - locked: true, - status: 'created', - }); - hook.project.delete.mockResolvedValue({ - results: { failed: false }, - project: Promise.resolve({ status: 'archived' }), - }); - const response = await archiveProject(project.id, user, reqId); - expect(response).toBeInstanceOf(ErrorResType); - expect(prisma.project.update).toHaveBeenCalledTimes(0); - }); - - it('should return hook fail', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ - id: projectId, - locked: false, - }); - hook.project.delete.mockResolvedValue({ - results: { failed: true }, - project: Promise.resolve({ status: 'failed' }), - }); - const response = await archiveProject(project.id, user, reqId); - expect(response).toBeInstanceOf(Unprocessable422); - }); - }); - - describe('generateProjectsData', () => { - it('shoud return string, very bad test ...', async () => { - prisma.project.findMany.mockResolvedValue([{ name: 'test' }]); - const response = await generateProjectsData(); - expect(response).toBeTypeOf('string'); - }); - }); -}); + }, + }, + }, + } + it('should return transform secret', async () => { + hook.project.getSecrets.mockResolvedValue(getResultsHook) + + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) + const response = await getProjectSecrets(projectId) + + // according to src/utils/mocks.ts + expect(JSON.stringify(response)).toContain('myToken') + }) + + it('should return projects secrets', async () => { + hook.project.getSecrets.mockResolvedValue(getResultsHook) + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) + prisma.project.findMany.mockResolvedValue({ id: projectId }) + const response = await getProjectSecrets(projectId) + // according to src/utils/mocks.ts + expect(JSON.stringify(response)).toContain('myToken') + }) + + it('should return hook error', async () => { + hook.project.getSecrets.mockResolvedValue({ failed: true }) + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) + prisma.project.findMany.mockResolvedValue({ id: projectId }) + const response = await getProjectSecrets(projectId) + // according to src/utils/mocks.ts + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + + describe('createProject', () => { + it('should create project', async () => { + logViaSessionMock.mockResolvedValue({ user }) + + prisma.project.create.mockResolvedValue({ ...project, status: 'initializing' }) + prisma.project.findFirst.mockResolvedValue(undefined) + prisma.project.findMany.mockResolvedValue([]) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const projectRes = await createProject(project, user, reqId) + + expect(projectRes.name).toEqual(project.name) + expect(prisma.project.create).toHaveBeenCalledTimes(1) + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + }) + + it('should return plugins failed', async () => { + logViaSessionMock.mockResolvedValue({ user }) + + prisma.project.create.mockResolvedValue({ ...project, status: 'initializing' }) + prisma.project.findFirst.mockResolvedValue(undefined) + prisma.project.findMany.mockResolvedValue([]) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const response = await createProject(project, user, reqId) + + expect(prisma.project.create).toHaveBeenCalledTimes(1) + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + describe('updateProject', () => { + const updatedProjet = { + description: faker.lorem.lines(2), + everyonePerms: '5', + } + const reqId = faker.string.uuid() + const members: ProjectMembers[] = [{ userId: faker.string.uuid(), projectId: project.id, roleIds: [], user: { type: 'human' } }] + it('should update project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members }) + prisma.project.update.mockResolvedValue(project) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + await updateProject({ ...updatedProjet, ownerId: members[0].userId }, project.id, user, reqId) + + expect(prisma.project.update).toHaveBeenCalledTimes(2) + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + }) + + it('should update nothing', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members }) + prisma.project.update.mockResolvedValue(project) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + await updateProject({ }, project.id, user, reqId) + + expect(prisma.project.update).toHaveBeenCalledTimes(0) + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + }) + + it('should not update if project archived', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, status: 'archived' }) + prisma.project.update.mockResolvedValue(project) + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + + const response = await updateProject({ }, project.id, user, reqId) + + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.project.update).toHaveBeenCalledTimes(0) + expect(prisma.log.create).toHaveBeenCalledTimes(0) + expect(hook.project.upsert).toHaveBeenCalledTimes(0) + }) + + it('should not update project, cause missing member', async () => { + hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) + logViaSessionMock.mockResolvedValue({ user }) + + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members: [] }) + + const response = await updateProject({ ownerId: members[0].userId }, project.id, user, reqId) + + expect(prisma.project.findUniqueOrThrow).toHaveBeenCalledTimes(1) + expect(response).toBeInstanceOf(BadRequest400) + expect(hook.project.upsert).toHaveBeenCalledTimes(0) + expect(prisma.log.update).toHaveBeenCalledTimes(0) + }) + + it('should return plugins failed', async () => { + logViaSessionMock.mockResolvedValue({ user }) + + prisma.project.findUniqueOrThrow.mockResolvedValue({ status: 'created' }) + hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) + + const response = await updateProject(updatedProjet, project.id, user, reqId) + + expect(prisma.project.update).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + describe('replayHooks', () => { + const reqId = faker.string.uuid() + + it('should replay hooks', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'created' }) + hook.project.upsert.mockResolvedValue({ results: { failed: false } }) + + await replayHooks(project.id, user, reqId) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + }) + + it('should not replay hooks on archived project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'archived' }) + hook.project.upsert.mockResolvedValue({ results: { failed: false } }) + + const response = await replayHooks(project.id, user, reqId) + + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.log.create).toHaveBeenCalledTimes(0) + expect(hook.project.upsert).toHaveBeenCalledTimes(0) + }) + + it('should not replay hooks on locked project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: true, status: 'created' }) + hook.project.upsert.mockResolvedValue({ results: { failed: false } }) + + const response = await replayHooks(project.id, user, reqId) + + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.log.create).toHaveBeenCalledTimes(0) + expect(hook.project.upsert).toHaveBeenCalledTimes(0) + }) + + it('should update nothing and return error', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'created' }) + hook.project.upsert.mockResolvedValue({ results: { failed: true } }) + + const response = await replayHooks(project.id, user, reqId) + + expect(prisma.log.create).toHaveBeenCalledTimes(1) + expect(hook.project.upsert).toHaveBeenCalledTimes(1) + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + + describe('archiveProject', () => { + it('should archive project', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false }) + hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) + const response = await archiveProject(project.id, user, reqId) + expect(response).toBeNull() + expect(prisma.project.update).toHaveBeenLastCalledWith({ + where: { id: project.id }, + data: { + clusters: { set: [] }, + }, + }) + }) + + it('should not archive a project already archived', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false, status: 'archived' }) + hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) + const response = await archiveProject(project.id, user, reqId) + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.project.update).toHaveBeenCalledTimes(0) + }) + + it('should not archive a project locked', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: true, status: 'created' }) + hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) + const response = await archiveProject(project.id, user, reqId) + expect(response).toBeInstanceOf(ErrorResType) + expect(prisma.project.update).toHaveBeenCalledTimes(0) + }) + + it('should return hook fail', async () => { + prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false }) + hook.project.delete.mockResolvedValue({ results: { failed: true }, project: Promise.resolve({ status: 'failed' }) }) + const response = await archiveProject(project.id, user, reqId) + expect(response).toBeInstanceOf(Unprocessable422) + }) + }) + + describe('generateProjectsData', () => { + it('shoud return string, very bad test ...', async () => { + prisma.project.findMany.mockResolvedValue([{ name: 'test' }]) + const response = await generateProjectsData() + expect(response).toBeTypeOf('string') + }) + }) +}) describe('chunk function', () => { - it('should return 5 elements', () => { - const letters = ['A', 'B', 'C', 'D', 'E']; - expect(chunk(letters, 5)).toEqual([letters]); - }); - it('should return 3,2 elements', () => { - const letters = ['A', 'B', 'C', 'D', 'E']; - expect(chunk(letters, 3)).toEqual([ - ['A', 'B', 'C'], - ['D', 'E'], - ]); - }); - it('should return 4 elements', () => { - const letters = ['A', 'B', 'C', 'D']; - expect(chunk(letters, 5)).toEqual([letters]); - }); -}); + it('should return 5 elements', () => { + const letters = ['A', 'B', 'C', 'D', 'E'] + expect(chunk(letters, 5)).toEqual([letters]) + }) + it('should return 3,2 elements', () => { + const letters = ['A', 'B', 'C', 'D', 'E'] + expect(chunk(letters, 3)).toEqual([['A', 'B', 'C'], ['D', 'E']]) + }) + it('should return 4 elements', () => { + const letters = ['A', 'B', 'C', 'D'] + expect(chunk(letters, 5)).toEqual([letters]) + }) +}) describe('generateSlug', () => { - it('should return prefix, no array', () => { - const prefix = faker.string.alphanumeric(5); - const generated = generateSlug(prefix); - expect(generated).toEqual(prefix); - }); - it('should return prefix, empty array', () => { - const prefix = faker.string.alphanumeric(5); - const generated = generateSlug(prefix, []); - expect(generated).toEqual(prefix); - }); - it('should return prefix, no match', () => { - const prefix = faker.string.alphanumeric(5); - const generated = generateSlug(prefix, [ - faker.string.alphanumeric(5), - faker.string.alphanumeric(5), - ]); - expect(generated).toEqual(prefix); - }); - it('should return generated slug at 1 or 0, all matchs', () => { - const prefix = faker.string.alphanumeric(5); - const generated = generateSlug(prefix, [prefix]); - expect(generated).match(/-[01]$/); - }); - it('should return generated slug at 4, all matchs', () => { - const prefix = faker.string.alphanumeric(5); - const generated = generateSlug(prefix, [ - prefix, - `${prefix}-0`, - `${prefix}-1`, - `${prefix}-2`, - `${prefix}-3`, - ]); - expect(generated).match(/-4$/); - }); - it('should fill empty space', () => { - const prefix = faker.string.alphanumeric(5); - const generated = generateSlug(prefix, [ - prefix, - `${prefix}-0`, - `${prefix}-1`, - `${prefix}-3`, - ]); - expect(generated).match(/-2$/); - }); -}); + it('should return prefix, no array', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix) + expect(generated).toEqual(prefix) + }) + it('should return prefix, empty array', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, []) + expect(generated).toEqual(prefix) + }) + it('should return prefix, no match', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, [faker.string.alphanumeric(5), faker.string.alphanumeric(5)]) + expect(generated).toEqual(prefix) + }) + it('should return generated slug at 1 or 0, all matchs', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, [prefix]) + expect(generated).match(/-[01]$/) + }) + it('should return generated slug at 4, all matchs', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, [prefix, `${prefix}-0`, `${prefix}-1`, `${prefix}-2`, `${prefix}-3`]) + expect(generated).match(/-4$/) + }) + it('should fill empty space', () => { + const prefix = faker.string.alphanumeric(5) + const generated = generateSlug(prefix, [prefix, `${prefix}-0`, `${prefix}-1`, `${prefix}-3`]) + expect(generated).match(/-2$/) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts index 607f6f2fb..2d1a92a67 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts @@ -1,405 +1,261 @@ -import { servicesInfos } from '@cpn-console/hooks'; -import type { projectContract } from '@cpn-console/shared'; -import { ProjectStatusSchema } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; +import { json2csv } from 'json-2-csv' +import { servicesInfos } from '@cpn-console/hooks' +import type { Project, User } from '@prisma/client' +import type { projectContract } from '@cpn-console/shared' +import { ProjectStatusSchema } from '@cpn-console/shared' import { - addLogs, - deleteAllEnvironmentForProject, - deleteAllRepositoryForProject, - getAllProjectsDataForExport, - getProjectOrThrow, - getSlugs, - initializeProject, - listProjects as listProjectsQuery, - lockProject, - updateProject as updateProjectQuery, -} from '@old-server/resources/queries-index'; -import type { UserDetails } from '@old-server/types/index'; -import { whereBuilder } from '@old-server/utils/controller'; -import { parallelBulkLimit } from '@old-server/utils/env'; -import type { ErrorResType } from '@old-server/utils/errors'; -import { - BadRequest400, - Forbidden403, - Unprocessable422, -} from '@old-server/utils/errors'; -import { hook } from '@old-server/utils/hook-wrapper'; -import type { Project, User } from '@prisma/client'; -import { json2csv } from 'json-2-csv'; + addLogs, + deleteAllEnvironmentForProject, + deleteAllRepositoryForProject, + getAllProjectsDataForExport, + getProjectOrThrow, + getSlugs, + initializeProject, + listProjects as listProjectsQuery, + lockProject, + updateProject as updateProjectQuery, +} from '@old-server/resources/queries-index' +import type { ErrorResType } from '@old-server/utils/errors' +import { BadRequest400, Forbidden403, Unprocessable422 } from '@old-server/utils/errors' +import { whereBuilder } from '@old-server/utils/controller' +import { hook } from '@old-server/utils/hook-wrapper' +import type { UserDetails } from '@old-server/types/index' +import prisma from '@old-server/prisma' +import { parallelBulkLimit } from '@old-server/utils/env' export function generateSlug(prefix: string, existingSlugs?: string[]) { - if (!existingSlugs?.includes(prefix)) { - return prefix; - } - let idx = 1; - let generated = `${prefix}-${idx}`; - while (existingSlugs.includes(generated)) { - idx++; - generated = `${prefix}-${idx}`; - } - return generated; + if (!existingSlugs?.includes(prefix)) { + return prefix + } + let idx = 1 + let generated = `${prefix}-${idx}` + while (existingSlugs.includes(generated)) { + idx++ + generated = `${prefix}-${idx}` + } + return generated } -const projectStatus = ProjectStatusSchema._def.values; -export async function listProjects( - { - status, - statusIn, - statusNotIn, - filter = 'member', - ...query - }: typeof projectContract.listProjects.query._type, - userId: User['id'] | undefined, -) { - return listProjectsQuery({ - ...query, - status: whereBuilder({ - enumValues: projectStatus, - eqValue: status, - inValues: statusIn, - notInValues: statusNotIn, - }), - filter, - userId, - }).then((projects) => - projects.map(({ clusters, ...project }) => ({ - ...project, - clusterIds: clusters.map(({ id }) => id), - roles: project.roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - everyonePerms: project.everyonePerms.toString(), - })), - ); +const projectStatus = ProjectStatusSchema._def.values +export async function listProjects({ status, statusIn, statusNotIn, filter = 'member', ...query }: typeof projectContract.listProjects.query._type, userId: User['id'] | undefined) { + return listProjectsQuery({ + ...query, + status: whereBuilder({ enumValues: projectStatus, eqValue: status, inValues: statusIn, notInValues: statusNotIn }), + filter, + userId, + }).then(projects => projects.map(({ clusters, ...project }) => ({ + ...project, + clusterIds: clusters.map(({ id }) => id), + roles: project.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), + everyonePerms: project.everyonePerms.toString(), + }))) } export async function getProjectSecrets(projectId: string) { - const hookReply = await hook.project.getSecrets(projectId); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services à la récupération des secrets du projet', - ); - } + const hookReply = await hook.project.getSecrets(projectId) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la récupération des secrets du projet') + } - return Object.fromEntries( - Object.entries(hookReply.results) - // @ts-ignore - .filter(([_key, value]) => Object.keys(value.secrets).length) - // @ts-ignore - .map(([key, value]) => [servicesInfos[key]?.title, value.secrets]), - ); + return Object.fromEntries( + Object.entries(hookReply.results) + // @ts-ignore + .filter(([_key, value]) => Object.keys(value.secrets).length) + // @ts-ignore + .map(([key, value]) => [servicesInfos[key]?.title, value.secrets]), + ) } -export async function createProject( - dataDto: typeof projectContract.createProject.body._type, - requestor: UserDetails, - requestId: string, -) { - if (requestor.type !== 'human') - return new BadRequest400( - 'Seuls les comptes humains peuvent créer des projets', - ); +export async function createProject(dataDto: typeof projectContract.createProject.body._type, requestor: UserDetails, requestId: string) { + if (requestor.type !== 'human') return new BadRequest400('Seuls les comptes humains peuvent créer des projets') - let slug = dataDto.name; - const projectsWithSamePrefix = await getSlugs(slug); - slug = generateSlug( - slug, - projectsWithSamePrefix?.map((project) => project.slug), - ); + let slug = dataDto.name + const projectsWithSamePrefix = await getSlugs(slug) + slug = generateSlug(slug, projectsWithSamePrefix?.map(project => project.slug)) - // Actions - const project = await initializeProject({ - ...dataDto, - slug, - ownerId: requestor.id, - }); + // Actions + const project = await initializeProject({ ...dataDto, slug, ownerId: requestor.id }) - const { results, project: projectInfos } = await hook.project.upsert( - project.id, - ); - await addLogs({ - action: 'Create Project', - data: results, - userId: requestor.id, - requestId, - projectId: project.id, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services à la création du projet', - ); - } + const { results, project: projectInfos } = await hook.project.upsert(project.id) + await addLogs({ action: 'Create Project', data: results, userId: requestor.id, requestId, projectId: project.id }) + if (results.failed) { + return new Unprocessable422('Echec des services à la création du projet') + } - return { - ...projectInfos, - clusterIds: projectInfos.clusters.map(({ id }) => id), - everyonePerms: projectInfos.everyonePerms.toString(), - roles: projectInfos.roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - }; + return { + ...projectInfos, + clusterIds: projectInfos.clusters.map(({ id }) => id), + everyonePerms: projectInfos.everyonePerms.toString(), + roles: projectInfos.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), + } } export async function getProject(projectId: Project['id']) { - return getProjectOrThrow(projectId).then(({ clusters, ...project }) => ({ - ...project, - clusterIds: clusters.map(({ id }) => id), - roles: project.roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - everyonePerms: project.everyonePerms.toString(), - })); + return getProjectOrThrow(projectId).then(({ clusters, ...project }) => ({ + ...project, + clusterIds: clusters.map(({ id }) => id), + roles: project.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), + everyonePerms: project.everyonePerms.toString(), + })) } export async function updateProject( - { - description, - ownerId: ownerIdCandidate, - everyonePerms, - locked, - ...data - }: typeof projectContract.updateProject.body._type, - projectId: Project['id'], - requestor: UserDetails, - requestId: string, + { description, ownerId: ownerIdCandidate, everyonePerms, locked, ...data }: typeof projectContract.updateProject.body._type, + projectId: Project['id'], + requestor: UserDetails, + requestId: string, ) { - // Actions - const projectDb = await prisma.project.findUniqueOrThrow({ - where: { id: projectId }, - include: { members: { include: { user: true } } }, - }); + // Actions + const projectDb = await prisma.project.findUniqueOrThrow({ + where: { id: projectId }, + include: { members: { include: { user: true } } }, + }) - if (projectDb.status === 'archived') - return new Forbidden403('Le projet est archivé'); + if (projectDb.status === 'archived') return new Forbidden403('Le projet est archivé') - if (ownerIdCandidate && ownerIdCandidate !== projectDb.ownerId) { - const memberCandidate = projectDb.members.find( - (member) => member.userId === ownerIdCandidate, - ); - if (!memberCandidate) { - return new BadRequest400( - 'Le nouveau propriétaire doit faire partie des membres actuels du projet', - ); - } - if (memberCandidate.user.type !== 'human') - return new BadRequest400( - 'Seuls les comptes humains peuvent être propriétaire de projets', - ); - if ( - !projectDb.members.find( - (member) => member.userId === projectDb.ownerId, - ) - ) { - await prisma.projectMembers.create({ - data: { userId: projectDb.ownerId, projectId }, - }); - } - await prisma.$transaction([ - prisma.projectMembers.delete({ - where: { - projectId_userId: { userId: ownerIdCandidate, projectId }, - }, - }), - prisma.project.update({ - where: { id: projectId }, - data: { ownerId: ownerIdCandidate }, - }), - ]); + if (ownerIdCandidate && ownerIdCandidate !== projectDb.ownerId) { + const memberCandidate = projectDb.members.find(member => member.userId === ownerIdCandidate) + if (!memberCandidate) { + return new BadRequest400('Le nouveau propriétaire doit faire partie des membres actuels du projet') } - - if ( - typeof description !== 'undefined' || - typeof everyonePerms !== 'undefined' || - typeof locked !== 'undefined' - ) { - await updateProjectQuery(projectId, { - description, - locked, - ...(everyonePerms && { everyonePerms: BigInt(everyonePerms) }), - ...data, - }); + if (memberCandidate.user.type !== 'human') return new BadRequest400('Seuls les comptes humains peuvent être propriétaire de projets') + if (!projectDb.members.find(member => member.userId === projectDb.ownerId)) { + await prisma.projectMembers.create({ + data: { userId: projectDb.ownerId, projectId }, + }) } + await prisma.$transaction([ + prisma.projectMembers.delete({ + where: { projectId_userId: { userId: ownerIdCandidate, projectId } }, + }), + prisma.project.update({ where: { id: projectId }, data: { ownerId: ownerIdCandidate } }), + ]) + } - const { results, project: projectInfos } = - await hook.project.upsert(projectId); - await addLogs({ - action: 'Update Project', - data: results, - userId: requestor.id, - requestId, - projectId: projectInfos.id, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services à la mise à jour du projet', - ); - } + if (typeof description !== 'undefined' || typeof everyonePerms !== 'undefined' || typeof locked !== 'undefined') { + await updateProjectQuery(projectId, { + description, + locked, + ...everyonePerms && { everyonePerms: BigInt(everyonePerms) }, + ...data, + }) + } - return { - ...projectInfos, - clusterIds: projectInfos.clusters.map(({ id }) => id), - everyonePerms: projectInfos.everyonePerms.toString(), - roles: projectInfos.roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - }; + const { results, project: projectInfos } = await hook.project.upsert(projectId) + await addLogs({ action: 'Update Project', data: results, userId: requestor.id, requestId, projectId: projectInfos.id }) + if (results.failed) { + return new Unprocessable422('Echec des services à la mise à jour du projet') + } + + return { + ...projectInfos, + clusterIds: projectInfos.clusters.map(({ id }) => id), + everyonePerms: projectInfos.everyonePerms.toString(), + roles: projectInfos.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), + } } interface ReplayHooksArgs { - projectId: Project['id']; - userId?: User['id']; - requestId: string; + projectId: Project['id'] + userId?: User['id'] + requestId: string } -export async function replayHooks({ - projectId, - userId, - requestId, -}: ReplayHooksArgs): Promise { - const projectDb = await prisma.project.findUniqueOrThrow({ - where: { id: projectId }, - include: { members: { include: { user: true } } }, - }); - if (projectDb.locked) return new Forbidden403('Le projet est verrouillé'); - if (projectDb.status === 'archived') - return new Forbidden403('Le projet est archivé'); - // Actions - const { results } = await hook.project.upsert(projectId); - await addLogs({ - action: 'Replay hooks for Project', - data: results, - userId, - requestId, - projectId, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services au reprovisionnement du projet', - ); - } - return null; +export async function replayHooks({ projectId, userId, requestId }: ReplayHooksArgs): Promise { + const projectDb = await prisma.project.findUniqueOrThrow({ + where: { id: projectId }, + include: { members: { include: { user: true } } }, + }) + if (projectDb.locked) return new Forbidden403('Le projet est verrouillé') + if (projectDb.status === 'archived') return new Forbidden403('Le projet est archivé') + // Actions + const { results } = await hook.project.upsert(projectId) + await addLogs({ action: 'Replay hooks for Project', data: results, userId, requestId, projectId }) + if (results.failed) { + return new Unprocessable422('Echec des services au reprovisionnement du projet') + } + return null } -export async function archiveProject( - projectId: Project['id'], - requestor: UserDetails, - requestId: string, -): Promise { - // Actions - // Empty the project first - const [projectDb, ..._] = await Promise.all([ - // get initial project state - prisma.project.findUniqueOrThrow({ where: { id: projectId } }), - deleteAllRepositoryForProject(projectId), - deleteAllEnvironmentForProject(projectId), - ]); +export async function archiveProject(projectId: Project['id'], requestor: UserDetails, requestId: string): Promise { + // Actions + // Empty the project first + const [projectDb, ..._] = await Promise.all([ + // get initial project state + prisma.project.findUniqueOrThrow({ where: { id: projectId } }), + deleteAllRepositoryForProject(projectId), + deleteAllEnvironmentForProject(projectId), + ]) - if (projectDb.locked) return new Forbidden403('Le projet est verrouillé'); - if (projectDb.status === 'archived') - return new BadRequest400('Le projet est archivé'); - if (projectDb.locked) { - await lockProject(projectId); - } + if (projectDb.locked) return new Forbidden403('Le projet est verrouillé') + if (projectDb.status === 'archived') return new BadRequest400('Le projet est archivé') + if (projectDb.locked) { + await lockProject(projectId) + } - // -- début - Suppression projet -- - const { results, project } = await hook.project.delete(projectId); - await addLogs({ - action: 'Delete all project resources', - data: results, - userId: requestor.id, - requestId, - projectId, - }); - if (project.status !== 'archived' && !projectDb.locked) { - await prisma.project.update({ - where: { id: projectId }, - data: { locked: false }, - }); - } - if (results.failed) { - return new Unprocessable422( - 'Echec des services à la suppression du projet', - ); - } + // -- début - Suppression projet -- + const { results, project } = await hook.project.delete(projectId) + await addLogs({ action: 'Delete all project resources', data: results, userId: requestor.id, requestId, projectId }) + if (project.status !== 'archived' && !projectDb.locked) { + await prisma.project.update({ where: { id: projectId }, data: { locked: false } }) + } + if (results.failed) { + return new Unprocessable422('Echec des services à la suppression du projet') + } - // Retrait clusters -- - await prisma.project.update({ - where: { id: projectId }, - data: { - clusters: { set: [] }, - }, - }); + // Retrait clusters -- + await prisma.project.update({ + where: { id: projectId }, + data: { + clusters: { set: [] }, + }, + }) - // -- fin - Suppression projet -- - return null; + // -- fin - Suppression projet -- + return null } export async function generateProjectsData() { - const projects = await getAllProjectsDataForExport(); + const projects = await getAllProjectsDataForExport() - return json2csv(projects, { - emptyFieldValue: '', - }); + return json2csv(projects, { + emptyFieldValue: '', + }) } -export async function bulkActionProject( - data: typeof projectContract.bulkActionProject.body._type, - requestor: UserDetails, - requestId: string, -) { - if (data.projectIds === 'all') { - data.projectIds = ( - await prisma.project.findMany({ - select: { id: true }, - where: { status: { not: 'archived' } }, - }) - ).map(({ id }) => id); - } - bulkExector( - data.projectIds.map((projectId) => { - if (data.action === 'archive') { - return () => archiveProject(projectId, requestor, requestId); - } - if (data.action === 'lock') { - return () => - updateProject( - { locked: true }, - projectId, - requestor, - requestId, - ); - } - if (data.action === 'unlock') { - return () => - updateProject( - { locked: false }, - projectId, - requestor, - requestId, - ); - } - if (data.action === 'replay') { - return () => - replayHooks({ projectId, userId: requestor.id, requestId }); - } - // should never been called - return async () => {}; - }), - ); +export async function bulkActionProject(data: typeof projectContract.bulkActionProject.body._type, requestor: UserDetails, requestId: string) { + if (data.projectIds === 'all') { + data.projectIds = (await prisma.project.findMany({ + select: { id: true }, + where: { status: { not: 'archived' } }, + })).map(({ id }) => id) + } + bulkExector(data.projectIds + .map((projectId) => { + if (data.action === 'archive') { + return () => archiveProject(projectId, requestor, requestId) + } + if (data.action === 'lock') { + return () => updateProject({ locked: true }, projectId, requestor, requestId) + } + if (data.action === 'unlock') { + return () => updateProject({ locked: false }, projectId, requestor, requestId) + } + if (data.action === 'replay') { + return () => replayHooks({ projectId, userId: requestor.id, requestId }) + } + // should never been called + return async () => {} + })) } export function chunk(arr: T[], size: number): T[][] { - return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => - arr.slice(i * size, i * size + size), - ); + return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => + arr.slice(i * size, i * size + size)) } async function bulkExector(toExecute: Array<() => Promise>) { - const toExecuteChunked = chunk(toExecute, parallelBulkLimit); - for (const chunkToExecute of toExecuteChunked) { - await Promise.allSettled(chunkToExecute.map((fn) => fn())); - } + const toExecuteChunked = chunk(toExecute, parallelBulkLimit) + for (const chunkToExecute of toExecuteChunked) { + await Promise.allSettled(chunkToExecute.map(fn => fn())) + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts index e0f68e66c..4cbf61b1a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts @@ -1,366 +1,335 @@ -import type { XOR, projectContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; -import { appVersion } from '@old-server/utils/env'; -import { uuid } from '@old-server/utils/queries-tools'; -import type { Prisma, Project, User } from '@prisma/client'; -import { ProjectStatus } from '@prisma/client'; +import type { + Prisma, + Project, + User, +} from '@prisma/client' +import { + ProjectStatus, +} from '@prisma/client' +import type { XOR, projectContract } from '@cpn-console/shared' +import prisma from '@old-server/prisma' +import { appVersion } from '@old-server/utils/env' +import { uuid } from '@old-server/utils/queries-tools' -type ProjectUpdate = Partial< - Pick ->; +type ProjectUpdate = Partial> export function updateProject(id: Project['id'], data: ProjectUpdate) { - return prisma.project.update({ - where: { id }, - data, - include: { members: true }, - }); + return prisma.project.update({ + where: { id }, + data, + include: { members: true }, + }) } // SELECT -type FilterWhere = XOR< - { - userId?: User['id']; - filter: 'all'; - }, - { - userId: User['id'] | undefined; - filter: 'owned' | 'member'; - } ->; -type ListProjectWhere = Omit< - typeof projectContract.listProjects.query._type, - 'status_in' | 'status_not_in' | 'status' -> & - Pick & - FilterWhere; +type FilterWhere = XOR<{ + userId?: User['id'] + filter: 'all' +}, { + userId: User['id'] | undefined + filter: 'owned' | 'member' + }> +type ListProjectWhere = Omit<(typeof projectContract.listProjects.query._type), 'status_in' | 'status_not_in' | 'status'> & + Pick & + FilterWhere export async function listProjects({ - description, - locked, - name, - status, - id, - filter, - userId, - search, - lastSuccessProvisionningVersion, + description, + locked, + name, + status, + id, + filter, + userId, + search, + lastSuccessProvisionningVersion, }: ListProjectWhere) { - const whereAnd: Prisma.ProjectWhereInput[] = []; - if (id) whereAnd.push({ id }); - if (locked != null) whereAnd.push({ locked }); - if (name) whereAnd.push({ name }); - if (status) whereAnd.push({ status }); - if (description) whereAnd.push({ description: { contains: description } }); - if (lastSuccessProvisionningVersion) { - if (lastSuccessProvisionningVersion === 'outdated') - whereAnd.push({ - lastSuccessProvisionningVersion: { not: appVersion }, - }); - else if (lastSuccessProvisionningVersion === 'last') - whereAnd.push({ - lastSuccessProvisionningVersion: { equals: appVersion }, - }); - else whereAnd.push({ lastSuccessProvisionningVersion }); - } - if (search) { - whereAnd.push({ - OR: [ - { - name: { contains: search }, - }, - { - owner: { email: { contains: search } }, - }, - ], - }); - } + const whereAnd: Prisma.ProjectWhereInput[] = [] + if (id) whereAnd.push({ id }) + if (locked != null) whereAnd.push({ locked }) + if (name) whereAnd.push({ name }) + if (status) whereAnd.push({ status }) + if (description) whereAnd.push({ description: { contains: description } }) + if (lastSuccessProvisionningVersion) { + if (lastSuccessProvisionningVersion === 'outdated') whereAnd.push({ lastSuccessProvisionningVersion: { not: appVersion } }) + else if (lastSuccessProvisionningVersion === 'last') whereAnd.push({ lastSuccessProvisionningVersion: { equals: appVersion } }) + else whereAnd.push({ lastSuccessProvisionningVersion }) + } + if (search) { + whereAnd.push({ OR: [{ + name: { contains: search }, + }, { + owner: { email: { contains: search } }, + }] }) + } - if (filter === 'owned') { - whereAnd.push({ ownerId: userId }); - } else if (filter === 'member') { - whereAnd.push({ - OR: [ - { - members: { some: { userId } }, - }, - { - ownerId: userId, - }, - ], - }); - } + if (filter === 'owned') { + whereAnd.push({ ownerId: userId }) + } else if (filter === 'member') { + whereAnd.push({ OR: [{ + members: { some: { userId } }, + }, { + ownerId: userId, + }] }) + } - return prisma.project.findMany({ - where: { AND: whereAnd }, - include: { - clusters: { select: { id: true } }, - members: { include: { user: true } }, - roles: true, - owner: true, - }, - }); + return prisma.project.findMany({ + where: { AND: whereAnd }, + include: { + clusters: { select: { id: true } }, + members: { include: { user: true } }, + roles: true, + owner: true, + }, + }) } export function getProjectOrThrow(id: Project['id'] | Project['slug']) { - return prisma.project.findFirstOrThrow({ - where: uuid.test(id) ? { id } : { slug: id }, - include: { - clusters: { select: { id: true } }, - members: { include: { user: true } }, - roles: true, - owner: true, - }, - }); + return prisma.project.findFirstOrThrow({ + where: uuid.test(id) + ? { id } + : { slug: id }, + include: { + clusters: { select: { id: true } }, + members: { include: { user: true } }, + roles: true, + owner: true, + }, + }) } export function getProjectInfosByIdOrThrow(projectId: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { - id: projectId, - }, - include: { - environments: true, - clusters: { include: { zone: true } }, - }, - }); + return prisma.project.findUniqueOrThrow({ + where: { + id: projectId, + }, + include: { + environments: true, + clusters: { include: { zone: true } }, + }, + }) } export function getProjectMembers(projectId: Project['id']) { - return prisma.projectMembers.findMany({ - where: { - projectId, - }, - include: { user: true }, - }); + return prisma.projectMembers.findMany({ + where: { + projectId, + }, + include: { user: true }, + }) } export function getProjectById(id: Project['id']) { - return prisma.project.findUnique({ where: { id } }); + return prisma.project.findUnique({ where: { id } }) } export const baseProjectIncludes = { - members: { include: { user: true } }, - clusters: true, - roles: true, - owner: true, -} as const; + members: { include: { user: true } }, + clusters: true, + roles: true, + owner: true, +} as const export function getProjectInfos(id: Project['id']) { - return prisma.project.findUnique({ - where: { id }, - include: baseProjectIncludes, - }); + return prisma.project.findUnique({ + where: { id }, + include: baseProjectIncludes, + }) } export function getProjectInfosOrThrow(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: baseProjectIncludes, - }); + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: baseProjectIncludes, + }) } export function getProjectInfosAndRepos(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: { - ...baseProjectIncludes, - repositories: true, - }, - }); + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: { + ...baseProjectIncludes, + repositories: true, + }, + }) } export function getSlugs(slugPrefix: string) { - return prisma.project.findMany({ - where: { - slug: { startsWith: slugPrefix }, - }, - }); + return prisma.project.findMany({ + where: { + slug: { startsWith: slugPrefix }, + }, + }) } export function getAllProjectsDataForExport() { - return prisma.project.findMany({ + return prisma.project.findMany({ + select: { + name: true, + description: true, + createdAt: true, + updatedAt: true, + environments: { select: { - name: true, - description: true, - createdAt: true, - updatedAt: true, - environments: { - select: { - name: true, - stage: true, - cluster: { - select: { label: true }, - }, - }, - }, - owner: true, + name: true, + stage: true, + cluster: { + select: { label: true }, + }, }, - }); + }, + owner: true, + }, + }) } export function getRolesByProjectId(projectId: Project['id']) { - return prisma.projectRole.findMany({ - where: { projectId }, - }); + return prisma.projectRole.findMany({ + where: { projectId }, + }) } const clusterInfosSelect = { - id: true, - infos: true, - label: true, - external: true, - privacy: true, - secretName: true, - kubeconfig: true, - clusterResources: true, - cpu: true, - gpu: true, - memory: true, - zone: { - select: { - id: true, - slug: true, - argocdUrl: true, - label: true, - }, + id: true, + infos: true, + label: true, + external: true, + privacy: true, + secretName: true, + kubeconfig: true, + clusterResources: true, + cpu: true, + gpu: true, + memory: true, + zone: { + select: { + id: true, + slug: true, + argocdUrl: true, + label: true, }, -}; + }, +} export function getHookProjectInfos(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: { + members: { include: { user: true }, where: { user: { type: 'human' } } }, + clusters: { select: clusterInfosSelect }, + environments: { include: { - members: { - include: { user: true }, - where: { user: { type: 'human' } }, - }, - clusters: { select: clusterInfosSelect }, - environments: { - include: { - stage: true, - cluster: { - select: clusterInfosSelect, - }, - }, - }, - repositories: true, - plugins: { - select: { - key: true, - pluginName: true, - value: true, - }, - }, - owner: true, - roles: true, + stage: true, + cluster: { + select: clusterInfosSelect, + }, }, - }); + }, + repositories: true, + plugins: { + select: { + key: true, + pluginName: true, + value: true, + }, + }, + owner: true, + roles: true, + }, + }) } // CREATE interface CreateProjectParams { - name: Project['name']; - description?: Project['description']; - ownerId: User['id']; - slug: Project['slug']; - limitless: boolean; - hprodCpu: number; - hprodGpu: number; - hprodMemory: number; - prodCpu: number; - prodGpu: number; - prodMemory: number; + name: Project['name'] + description?: Project['description'] + ownerId: User['id'] + slug: Project['slug'] + limitless: boolean + hprodCpu: number + hprodGpu: number + hprodMemory: number + prodCpu: number + prodGpu: number + prodMemory: number } export function initializeProject(params: CreateProjectParams) { - return prisma.project.create({ - data: { - description: params.description ?? '', - status: ProjectStatus.created, - locked: false, - ...params, - }, - }); + return prisma.project.create({ + data: { + description: params.description ?? '', + status: ProjectStatus.created, + locked: false, + ...params, + }, + }) } // UPDATE export function lockProject(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { locked: true }, - }); + return prisma.project.update({ + where: { id }, + data: { locked: true }, + }) } export function updateProjectCreated(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { - status: ProjectStatus.created, - lastSuccessProvisionningVersion: appVersion, - }, - include: baseProjectIncludes, - }); + return prisma.project.update({ + where: { id }, + data: { + status: ProjectStatus.created, + lastSuccessProvisionningVersion: appVersion, + }, + include: baseProjectIncludes, + }) } export function updateProjectFailed(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { status: ProjectStatus.failed }, - include: baseProjectIncludes, - }); + return prisma.project.update({ + where: { id }, + data: { status: ProjectStatus.failed }, + include: baseProjectIncludes, + }) } export function updateProjectWarning(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { status: ProjectStatus.warning }, - include: baseProjectIncludes, - }); + return prisma.project.update({ + where: { id }, + data: { status: ProjectStatus.warning }, + include: baseProjectIncludes, + }) } -export function addUserToProject({ - project, - user, -}: { - project: Project; - user: User; -}) { - return prisma.projectMembers.create({ - data: { - userId: user.id, - projectId: project.id, - }, - }); +export function addUserToProject({ project, user }: { project: Project, user: User }) { + return prisma.projectMembers.create({ + data: { + userId: user.id, + projectId: project.id, + }, + }) } -export function removeUserFromProject({ - projectId, - userId, -}: { - projectId: Project['id']; - userId: User['id']; -}) { - return prisma.projectMembers.delete({ - where: { - projectId_userId: { - projectId, - userId, - }, - }, - }); +export function removeUserFromProject({ projectId, userId }: { projectId: Project['id'], userId: User['id'] }) { + return prisma.projectMembers.delete({ + where: { + projectId_userId: { + projectId, + userId, + }, + }, + }) } export async function archiveProject(id: Project['id']) { - const project = await prisma.project.findUnique({ - where: { id }, - select: { name: true, slug: true }, - }); - return prisma.project.update({ - where: { id }, - data: { - name: `${project?.name}_${Date.now()}_archived`, - slug: `${project?.slug}_${Date.now()}_archived`, - status: ProjectStatus.archived, - locked: true, - }, - include: baseProjectIncludes, - }); + const project = await prisma.project.findUnique({ + where: { id }, + select: { name: true, slug: true }, + }) + return prisma.project.update({ + where: { id }, + data: { + name: `${project?.name}_${Date.now()}_archived`, + slug: `${project?.slug}_${Date.now()}_archived`, + status: ProjectStatus.archived, + locked: true, + }, + include: baseProjectIncludes, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts index fd0be47b4..cb1362cbe 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts @@ -1,661 +1,440 @@ -import type { ProjectV2 } from '@cpn-console/shared'; -import { PROJECT_PERMS, projectContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import type { UserDetails } from '../../types/index'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { - getProjectMockInfos, - getRandomRequestor, - getUserMockInfos, -} from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessListMock = vi.spyOn(business, 'listProjects'); -const businessCreateMock = vi.spyOn(business, 'createProject'); -const businessUpdateMock = vi.spyOn(business, 'updateProject'); -const businessDeleteMock = vi.spyOn(business, 'archiveProject'); -const businessSyncMock = vi.spyOn(business, 'replayHooks'); -const bulkActionProjectMock = vi.spyOn(business, 'bulkActionProject'); -const businessGetSecretsMock = vi.spyOn(business, 'getProjectSecrets'); -const businessGenerateDataMock = vi.spyOn(business, 'generateProjectsData'); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ProjectV2 } from '@cpn-console/shared' +import { PROJECT_PERMS, projectContract } from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getProjectMockInfos, getRandomRequestor, getUserMockInfos } from '../../utils/mocks' +import { BadRequest400 } from '../../utils/errors' +import * as business from './business' +import type { UserDetails } from '../../types/index' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMock = vi.spyOn(business, 'listProjects') +const businessCreateMock = vi.spyOn(business, 'createProject') +const businessUpdateMock = vi.spyOn(business, 'updateProject') +const businessDeleteMock = vi.spyOn(business, 'archiveProject') +const businessSyncMock = vi.spyOn(business, 'replayHooks') +const bulkActionProjectMock = vi.spyOn(business, 'bulkActionProject') +const businessGetSecretsMock = vi.spyOn(business, 'getProjectSecrets') +const businessGenerateDataMock = vi.spyOn(business, 'generateProjectsData') describe('test projectContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - const projectOwner: ProjectV2['owner'] = { - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - id: faker.string.uuid(), - type: 'human', - }; - const projectId = faker.string.uuid(); - const project: Omit = { - name: faker.string.alpha({ length: 10, casing: 'lower' }), - slug: faker.string.alpha({ length: 5, casing: 'lower' }), - description: faker.string.alpha({ length: 5 }), - limitless: false, - hprodCpu: faker.number.int({ min: 0, max: 1000 }), - hprodGpu: faker.number.int({ min: 0, max: 1000 }), - hprodMemory: faker.number.int({ min: 0, max: 1000 }), - prodCpu: faker.number.int({ min: 0, max: 1000 }), - prodGpu: faker.number.int({ min: 0, max: 1000 }), - prodMemory: faker.number.int({ min: 0, max: 1000 }), - clusterIds: [], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - locked: false, - status: 'created', - everyonePerms: '0', - members: [], - owner: projectOwner, - ownerId: projectOwner.id, - roles: [], - lastSuccessProvisionningVersion: null, - }; - describe('check unauthorized user on project behaviour', () => { - // UPDATE - it('on Update', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - projectContract.updateProject.path.replace( - ':projectId', - projectId, - ), - ) - .body(project) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(404); - expect(response.json()).toEqual({ message: 'Not Found' }); - }); - - it('on Update without enough perms', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - projectContract.updateProject.path.replace( - ':projectId', - projectId, - ), - ) - .body(project) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ message: 'Forbidden' }); - }); - - // REPLAY - it('on replay', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - projectContract.replayHooksForProject.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(businessSyncMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(404); - expect(response.json()).toEqual({ message: 'Not Found' }); - }); - - // SECRETS - it('on see secret', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - projectContract.getProjectSecrets.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(businessGetSecretsMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(404); - expect(response.json()).toEqual({ message: 'Not Found' }); - }); - - // ARCHIVE - it('on archive', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - projectContract.archiveProject.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(businessDeleteMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(404); - expect(response.json()).toEqual({ message: 'Not Found' }); - }); - }); - describe('listProjects', () => { - it('should return list of projects', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - const projects = []; - businessListMock.mockResolvedValueOnce(projects); - const response = await app - .inject() - .get(projectContract.listProjects.path) - .end(); - - expect(businessListMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(projects); - expect(response.statusCode).toEqual(200); - }); - it('should return 400 for non-admin with "all" filter', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - const response = await app - .inject() - .get(`${projectContract.listProjects.path}?filter=all`) - .end(); - - expect(response.statusCode).toEqual(400); - }); - }); - - describe('createProject', () => { - it('should create and return project for authorized user', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce({ - id: projectId, - ...project, - }); - const response = await app - .inject() - .post(projectContract.createProject.path) - .body(project) - .end(); - - expect(businessCreateMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual({ id: projectId, ...project }); - expect(response.statusCode).toEqual(201); - }); - - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .post(projectContract.createProject.path) - .body(project) - .end(); - - expect(response.statusCode).toEqual(400); - }); - }); - - describe('updateProject', () => { - const projectUpdated: Partial = { - description: faker.string.alpha({ length: 5 }), - }; - - it('should update and return project for authorized user', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce({ - id: projectId, - ...project, - ...projectUpdated, - }); - const response = await app - .inject() - .put( - projectContract.updateProject.path.replace( - ':projectId', - projectId, - ), - ) - .body(projectUpdated) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual({ - id: projectId, - ...project, - ...projectUpdated, - }); - expect(response.statusCode).toEqual(200); - }); - - it('should not update ownerId if not permitted', async () => { - const userDetails = getRandomRequestor(); - const projectPerms = getProjectMockInfos({ - projectOwnerId: faker.string.uuid(), - projectPermissions: PROJECT_PERMS.MANAGE, - }); - const projectUpdated = { - ownerId: faker.string.uuid(), - description: faker.lorem.words(), - }; - const user = getUserMockInfos( - false, - userDetails as UserDetails, - projectPerms, - ); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce({ - id: projectId, - ...project, - ...projectUpdated, - }); - const response = await app - .inject() - .put( - projectContract.updateProject.path.replace( - ':projectId', - projectId, - ), - ) - .body(projectUpdated) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledWith( - { description: projectUpdated.description }, - projectId, - user.user, - expect.any(String), - ); - expect(response.json()).toEqual({ - id: projectId, - ...project, - ...projectUpdated, - }); - expect(response.statusCode).toEqual(200); - }); - - it('should update ownerId and return project', async () => { - const requestor = getRandomRequestor(); - const projectPerms = getProjectMockInfos({ - projectOwnerId: requestor.id, - projectPermissions: PROJECT_PERMS.MANAGE, - }); - const projectUpdated = { - ownerId: faker.string.uuid(), - description: faker.lorem.words(), - }; - const user = getUserMockInfos( - false, - requestor as UserDetails, - projectPerms, - ); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce({ - id: projectId, - ...project, - ...projectUpdated, - }); - const response = await app - .inject() - .put( - projectContract.updateProject.path.replace( - ':projectId', - projectId, - ), - ) - .body(projectUpdated) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledWith( - projectUpdated, - projectId, - user.user, - expect.any(String), - ); - expect(response.json()).toEqual({ - id: projectId, - ...project, - ...projectUpdated, - }); - expect(response.statusCode).toEqual(200); - }); - - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .put( - projectContract.updateProject.path.replace( - ':projectId', - projectId, - ), - ) - .body(project) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledTimes(1); - expect(response.statusCode).toEqual(400); - }); - }); - - describe('archiveProject', () => { - it('should archive project for authorized user', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce(null); - const response = await app - .inject() - .delete( - projectContract.archiveProject.path.replace( - ':projectId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessDeleteMock).toHaveBeenCalledTimes(1); - expect(response.body).toBeFalsy(); - expect(response.statusCode).toEqual(204); - }); - - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .delete( - projectContract.archiveProject.path.replace( - ':projectId', - faker.string.uuid(), - ), - ) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return projects data for admin', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - projectContract.archiveProject.path.replace( - ':projectId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessDeleteMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('getProjectSecrets', () => { - it('should return project secrets for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.SEE_SECRETS, - }); - const user = getUserMockInfos(true, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const secrets = {}; - businessGetSecretsMock.mockResolvedValueOnce(secrets); - const response = await app - .inject() - .get( - projectContract.getProjectSecrets.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(businessGetSecretsMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(secrets); - expect(response.statusCode).toEqual(200); - }); - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE, - }); - const user = getUserMockInfos(true, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessGetSecretsMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .get( - projectContract.getProjectSecrets.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for unauthorized access to secrets', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - projectContract.getProjectSecrets.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(403); - }); - }); - - describe('replayHooksForProject', () => { - it('should replay hooks for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE, - }); - const user = getUserMockInfos(true, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessSyncMock.mockResolvedValueOnce(null); - const response = await app - .inject() - .put( - projectContract.replayHooksForProject.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(businessSyncMock).toHaveBeenCalledTimes(1); - expect(response.body).toBeFalsy(); - expect(response.statusCode).toEqual(204); - }); - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE, - }); - const user = getUserMockInfos(true, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessSyncMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .put( - projectContract.replayHooksForProject.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for unauthorized access to replay hooks', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - const response = await app - .inject() - .put( - projectContract.replayHooksForProject.path.replace( - ':projectId', - projectId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(403); - }); - }); - - describe('getProjectsData', () => { - it('should return projects data for admin', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - const data = ''; - businessGenerateDataMock.mockResolvedValueOnce(data); - const response = await app - .inject() - .get(projectContract.getProjectsData.path) - .end(); - - expect(businessGenerateDataMock).toHaveBeenCalledTimes(1); - expect(response.body).toEqual(data); - expect(response.statusCode).toEqual(200); - }); - - it('should return 403 for non-admin user', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(projectContract.getProjectsData.path) - .end(); - - expect(response.statusCode).toEqual(403); - }); - }); - - describe('bulkActionProject', () => { - it('should executebulk for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE, - }); - const user = getUserMockInfos(true, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessSyncMock.mockResolvedValueOnce(null); - const response = await app - .inject() - .post(projectContract.bulkActionProject.path) - .body({ action: 'lock', projectIds: [projectId] }) - .end(); - - expect(response.json()).toBeNull(); - expect(bulkActionProjectMock).toHaveBeenCalledTimes(1); - expect(response.statusCode).toEqual(202); - }); - - it('should return 403 for unauthorized access to bulk update', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - const response = await app - .inject() - .post(projectContract.bulkActionProject.path) - .body({ action: 'lock', projectIds: [projectId] }) - .end(); - - expect(response.statusCode).toEqual(403); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + const projectOwner: ProjectV2['owner'] = { + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + id: faker.string.uuid(), + type: 'human', + } + const projectId = faker.string.uuid() + const project: Omit = { + name: faker.string.alpha({ length: 10, casing: 'lower' }), + slug: faker.string.alpha({ length: 5, casing: 'lower' }), + description: faker.string.alpha({ length: 5 }), + limitless: false, + hprodCpu: faker.number.int({ min: 0, max: 1000 }), + hprodGpu: faker.number.int({ min: 0, max: 1000 }), + hprodMemory: faker.number.int({ min: 0, max: 1000 }), + prodCpu: faker.number.int({ min: 0, max: 1000 }), + prodGpu: faker.number.int({ min: 0, max: 1000 }), + prodMemory: faker.number.int({ min: 0, max: 1000 }), + clusterIds: [], + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + locked: false, + status: 'created', + everyonePerms: '0', + members: [], + owner: projectOwner, + ownerId: projectOwner.id, + roles: [], + lastSuccessProvisionningVersion: null, + } + describe('check unauthorized user on project behaviour', () => { + // UPDATE + it('on Update', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(project) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(404) + expect(response.json()).toEqual({ message: 'Not Found' }) + }) + + it('on Update without enough perms', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(project) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Forbidden' }) + }) + + // REPLAY + it('on replay', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) + .end() + + expect(businessSyncMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(404) + expect(response.json()).toEqual({ message: 'Not Found' }) + }) + + // SECRETS + it('on see secret', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) + .end() + + expect(businessGetSecretsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(404) + expect(response.json()).toEqual({ message: 'Not Found' }) + }) + + // ARCHIVE + it('on archive', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectContract.archiveProject.path.replace(':projectId', projectId)) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(404) + expect(response.json()).toEqual({ message: 'Not Found' }) + }) + }) + describe('listProjects', () => { + it('should return list of projects', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + const projects = [] + businessListMock.mockResolvedValueOnce(projects) + const response = await app.inject() + .get(projectContract.listProjects.path) + .end() + + expect(businessListMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(projects) + expect(response.statusCode).toEqual(200) + }) + it('should return 400 for non-admin with "all" filter', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + const response = await app.inject() + .get(`${projectContract.listProjects.path}?filter=all`) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('createProject', () => { + it('should create and return project for authorized user', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce({ id: projectId, ...project }) + const response = await app.inject() + .post(projectContract.createProject.path) + .body(project) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ id: projectId, ...project }) + expect(response.statusCode).toEqual(201) + }) + + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(projectContract.createProject.path) + .body(project) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('updateProject', () => { + const projectUpdated: Partial = { description: faker.string.alpha({ length: 5 }) } + + it('should update and return project for authorized user', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(projectUpdated) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) + expect(response.statusCode).toEqual(200) + }) + + it('should not update ownerId if not permitted', async () => { + const userDetails = getRandomRequestor() + const projectPerms = getProjectMockInfos({ projectOwnerId: faker.string.uuid(), projectPermissions: PROJECT_PERMS.MANAGE }) + const projectUpdated = { ownerId: faker.string.uuid(), description: faker.lorem.words() } + const user = getUserMockInfos(false, userDetails as UserDetails, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(projectUpdated) + .end() + + expect(businessUpdateMock).toHaveBeenCalledWith({ description: projectUpdated.description }, projectId, user.user, expect.any(String)) + expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) + expect(response.statusCode).toEqual(200) + }) + + it('should update ownerId and return project', async () => { + const requestor = getRandomRequestor() + const projectPerms = getProjectMockInfos({ projectOwnerId: requestor.id, projectPermissions: PROJECT_PERMS.MANAGE }) + const projectUpdated = { ownerId: faker.string.uuid(), description: faker.lorem.words() } + const user = getUserMockInfos(false, requestor as UserDetails, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(projectUpdated) + .end() + + expect(businessUpdateMock).toHaveBeenCalledWith(projectUpdated, projectId, user.user, expect.any(String)) + expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) + expect(response.statusCode).toEqual(200) + }) + + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(projectContract.updateProject.path.replace(':projectId', projectId)) + .body(project) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(1) + expect(response.statusCode).toEqual(400) + }) + }) + + describe('archiveProject', () => { + it('should archive project for authorized user', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(1) + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return projects data for admin', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getProjectSecrets', () => { + it('should return project secrets for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const secrets = {} + businessGetSecretsMock.mockResolvedValueOnce(secrets) + const response = await app.inject() + .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) + .end() + + expect(businessGetSecretsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(secrets) + expect(response.statusCode).toEqual(200) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetSecretsMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for unauthorized access to secrets', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('replayHooksForProject', () => { + it('should replay hooks for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(null) + const response = await app.inject() + .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) + .end() + + expect(businessSyncMock).toHaveBeenCalledTimes(1) + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for unauthorized access to replay hooks', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + const response = await app.inject() + .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('getProjectsData', () => { + it('should return projects data for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + const data = '' + businessGenerateDataMock.mockResolvedValueOnce(data) + const response = await app.inject() + .get(projectContract.getProjectsData.path) + .end() + + expect(businessGenerateDataMock).toHaveBeenCalledTimes(1) + expect(response.body).toEqual(data) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for non-admin user', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(projectContract.getProjectsData.path) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) + + describe('bulkActionProject', () => { + it('should executebulk for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) + const user = getUserMockInfos(true, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(null) + const response = await app.inject() + .post(projectContract.bulkActionProject.path) + .body({ action: 'lock', projectIds: [projectId] }) + .end() + + expect(response.json()).toBeNull() + expect(bulkActionProjectMock).toHaveBeenCalledTimes(1) + expect(response.statusCode).toEqual(202) + }) + + it('should return 403 for unauthorized access to bulk update', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + const response = await app.inject() + .post(projectContract.bulkActionProject.path) + .body({ action: 'lock', projectIds: [projectId] }) + .end() + + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts index ffdbeacc5..497b3f860 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts @@ -1,252 +1,199 @@ -import type { AsyncReturnType } from '@cpn-console/shared'; +import type { AsyncReturnType } from '@cpn-console/shared' +import { AdminAuthorized, ProjectAuthorized, projectContract } from '@cpn-console/shared' import { - AdminAuthorized, - ProjectAuthorized, - projectContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { - BadRequest400, - ErrorResType, - Forbidden403, - NotFound404, - Unauthorized401, -} from '@old-server/utils/errors'; - -import { - archiveProject, - bulkActionProject, - createProject, - generateProjectsData, - getProject, - getProjectSecrets, - listProjects, - replayHooks, - updateProject, -} from './business'; - -@Injectable() -export class ProjectRouterService { - constructor(private readonly appService: AppService) {} - - projectRouter() { - return this.appService.serverInstance.router(projectContract, { - // Récupérer des projets - listProjects: async ({ request: req, query }) => { - const { adminPermissions, user } = await authUser(req); - let body: AsyncReturnType = []; - - if (adminPermissions && !user) { - // c'est donc un compte de service - query.filter = 'all'; - } - if ( - query.filter === 'all' && - !AdminAuthorized.isAdmin(adminPermissions) - ) { - return new BadRequest400( - "Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre 'all'", - ); - } - - body = await listProjects(query, user?.id); - - return { - status: 200, - body, - }; - }, - - // Récupérer les secrets d'un projet - getProjectSecrets: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.SeeSecrets(perms)) - return new Forbidden403(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await getProjectSecrets(projectId); - - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Créer un projet - createProject: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (perms.user?.type !== 'human') - return new Unauthorized401( - 'Cannot find requestor in database', - ); - const body = await createProject(data, perms.user, req.id); - - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Récuperer un seul projet - getProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.projectId) return new NotFound404(); - if (!isAdmin) { - if (!perms.projectPermissions) { - return new NotFound404(); - } - if (perms.projectStatus === 'archived') { - return new NotFound404(); - } - } - - const body = await getProject(projectId); - - return { - status: 200, - body, - }; - }, - - // Mettre à jour un projet - updateProject: async ({ request: req, params, body: data }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) - return new Unauthorized401( - 'Cannot find requestor in database', - ); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - const isOwner = perms.projectOwnerId === perms.user.id; - - if (!perms.projectPermissions && !isAdmin) - return new NotFound404(); - if (!isAdmin) { - // filtrage des clés par niveau de permissions - delete data.locked; - if (!isOwner) { - delete data.ownerId; // impossible de toucher à cette clé - } - } - if (perms.projectLocked) { - if (!isAdmin) - return new Forbidden403('Le projet est verrouillé'); - if (data.locked !== false) - return new Forbidden403( - 'Veuillez déverrouiler le projet pour le mettre à jour', - ); - } - - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); - - const body = await updateProject( - data, - projectId, - perms.user, - req.id, - ); - - if (body instanceof ErrorResType) return body; - return { - status: 200, - body, - }; - }, - - // Reprovisionner un projet - replayHooksForProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.projectPermissions && !isAdmin) - return new NotFound404(); - if (!ProjectAuthorized.ReplayHooks(perms)) - return new Forbidden403(); - - const body = await replayHooks({ - projectId, - userId: perms.user?.id, - requestId: req.id, - }); - - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - - // Archiver un projet - archiveProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.user) - return new Unauthorized401( - 'Cannot find requestor in database', - ); - if (!perms.projectPermissions && !isAdmin) - return new NotFound404(); - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); - - const body = await archiveProject( - projectId, - perms.user, - req.id, - ); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - // Récupérer les données de tous les projets pour export - getProjectsData: async ({ request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await generateProjectsData(); - - return { - status: 200, - body, - }; - }, - - bulkActionProject: async ({ request: req, body }) => { - const perms = await authUser(req); - - if (!perms.user) - return new Unauthorized401( - 'Cannot find requestor in database', - ); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - await bulkActionProject(body, perms.user, req.id); - - return { - status: 202, - body: null, - }; - }, - }); - } + archiveProject, + bulkActionProject, + createProject, + generateProjectsData, + getProject, + getProjectSecrets, + listProjects, + replayHooks, + updateProject, +} from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' + +export function projectRouter() { + return serverInstance.router(projectContract, { + + // Récupérer des projets + listProjects: async ({ request: req, query }) => { + const { adminPermissions, user } = await authUser(req) + let body: AsyncReturnType = [] + + if (adminPermissions && !user) { // c'est donc un compte de service + query.filter = 'all' + } + if (query.filter === 'all' && !AdminAuthorized.isAdmin(adminPermissions)) { + return new BadRequest400('Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre \'all\'') + } + + body = await listProjects( + query, + user?.id, + ) + + return { + status: 200, + body, + } + }, + + // Récupérer les secrets d'un projet + getProjectSecrets: async ({ request: req, params }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.SeeSecrets(perms)) return new Forbidden403() + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const body = await getProjectSecrets(projectId) + + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + // Créer un projet + createProject: async ({ request: req, body: data }) => { + const perms = await authUser(req) + if (perms.user?.type !== 'human') return new Unauthorized401('Cannot find requestor in database') + const body = await createProject(data, perms.user, req.id) + + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + // Récuperer un seul projet + getProject: async ({ request: req, params }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + if (!perms.projectId) return new NotFound404() + if (!isAdmin) { + if (!perms.projectPermissions) { + return new NotFound404() + } + if (perms.projectStatus === 'archived') { + return new NotFound404() + } + } + + const body = await getProject(projectId) + + return { + status: 200, + body, + } + }, + + // Mettre à jour un projet + updateProject: async ({ request: req, params, body: data }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + + if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + const isOwner = perms.projectOwnerId === perms.user.id + + if (!perms.projectPermissions && !isAdmin) return new NotFound404() + if (!isAdmin) { // filtrage des clés par niveau de permissions + delete data.locked + if (!isOwner) { + delete data.ownerId // impossible de toucher à cette clé + } + } + if (perms.projectLocked) { + if (!isAdmin) return new Forbidden403('Le projet est verrouillé') + if (data.locked !== false) return new Forbidden403('Veuillez déverrouiler le projet pour le mettre à jour') + } + + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() + + const body = await updateProject(data, projectId, perms.user, req.id) + + if (body instanceof ErrorResType) return body + return { + status: 200, + body, + } + }, + + // Reprovisionner un projet + replayHooksForProject: async ({ request: req, params }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + if (!perms.projectPermissions && !isAdmin) return new NotFound404() + if (!ProjectAuthorized.ReplayHooks(perms)) return new Forbidden403() + + const body = await replayHooks({ + projectId, + userId: perms.user?.id, + requestId: req.id, + }) + + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + + // Archiver un projet + archiveProject: async ({ request: req, params }) => { + const projectId = params.projectId + const perms = await authUser(req, { id: projectId }) + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + if (!perms.projectPermissions && !isAdmin) return new NotFound404() + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() + + const body = await archiveProject(projectId, perms.user, req.id) + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + // Récupérer les données de tous les projets pour export + getProjectsData: async ({ request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + const body = await generateProjectsData() + + return { + status: 200, + body, + } + }, + + bulkActionProject: async ({ request: req, body }) => { + const perms = await authUser(req) + + if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + await bulkActionProject(body, perms.user, req.id) + + return { + status: 202, + body: null, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts index d3dd04328..7923b2216 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts @@ -1,14 +1,14 @@ -export * from '@old-server/resources/admin-role/queries'; -export * from '@old-server/resources/cluster/queries'; -export * from '@old-server/resources/service-chain/queries'; -export * from '@old-server/resources/environment/queries'; -export * from '@old-server/resources/log/queries'; -export * from '@old-server/resources/project/queries'; -export * from '@old-server/resources/project-member/queries'; -export * from '@old-server/resources/project-role/queries'; -export * from '@old-server/resources/project-service/queries'; -export * from '@old-server/resources/repository/queries'; -export * from '@old-server/resources/user/queries'; -export * from '@old-server/resources/stage/queries'; -export * from '@old-server/resources/zone/queries'; -export * from '@old-server/resources/system/settings/queries'; +export * from '@old-server/resources/admin-role/queries' +export * from '@old-server/resources/cluster/queries' +export * from '@old-server/resources/service-chain/queries' +export * from '@old-server/resources/environment/queries' +export * from '@old-server/resources/log/queries' +export * from '@old-server/resources/project/queries' +export * from '@old-server/resources/project-member/queries' +export * from '@old-server/resources/project-role/queries' +export * from '@old-server/resources/project-service/queries' +export * from '@old-server/resources/repository/queries' +export * from '@old-server/resources/user/queries' +export * from '@old-server/resources/stage/queries' +export * from '@old-server/resources/zone/queries' +export * from '@old-server/resources/system/settings/queries' diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts index 7c6433828..207548951 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts @@ -1,179 +1,115 @@ -import type { - CreateRepositoryBody, - UpdateRepositoryBody, -} from '@cpn-console/shared'; -import { - addLogs, - deleteRepository as deleteRepositoryQuery, - getProjectInfosAndRepos, - getProjectRepositories as getProjectRepositoriesQuery, - initializeRepository, - updateRepository as updateRepositoryQuery, -} from '@old-server/resources/queries-index'; -import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors'; -import { hook } from '@old-server/utils/hook-wrapper'; -import type { Project, Repository, User } from '@prisma/client'; +import type { Project, Repository, User } from '@prisma/client' +import type { CreateRepositoryBody, UpdateRepositoryBody } from '@cpn-console/shared' +import { addLogs, deleteRepository as deleteRepositoryQuery, getProjectInfosAndRepos, getProjectRepositories as getProjectRepositoriesQuery, initializeRepository, updateRepository as updateRepositoryQuery } from '@old-server/resources/queries-index' +import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors' +import { hook } from '@old-server/utils/hook-wrapper' export async function getProjectRepositories(projectId: Project['id']) { - return getProjectRepositoriesQuery(projectId); + return getProjectRepositoriesQuery(projectId) } export async function syncRepository({ - repositoryId, - userId, - syncAllBranches, - branchName, - requestId, + repositoryId, + userId, + syncAllBranches, + branchName, + requestId, }: { - repositoryId: Repository['id']; - userId: User['id']; - syncAllBranches: boolean; - branchName?: string; - requestId: string; + repositoryId: Repository['id'] + userId: User['id'] + syncAllBranches: boolean + branchName?: string + requestId: string }) { - const hookReply = await hook.misc.syncRepository(repositoryId, { - syncAllBranches, - branchName, - }); - await addLogs({ - action: 'Sync Repository', - data: hookReply, - userId, - requestId, - projectId: hookReply.args.id, - }); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services à la synchronisation du dépôt', - ); - } - return null; + const hookReply = await hook.misc.syncRepository(repositoryId, { syncAllBranches, branchName }) + await addLogs({ action: 'Sync Repository', data: hookReply, userId, requestId, projectId: hookReply.args.id }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services à la synchronisation du dépôt') + } + return null } export async function createRepository({ - data, - userId, - requestId, + data, + userId, + requestId, }: { - data: CreateRepositoryBody; - userId: User['id']; - requestId: string; + data: CreateRepositoryBody + userId: User['id'] + requestId: string }) { - const project = await getProjectInfosAndRepos(data.projectId); + const project = await getProjectInfosAndRepos(data.projectId) - if ( - project.repositories?.find( - (repo) => repo.internalRepoName === data.internalRepoName, - ) - ) - return new BadRequest400( - `Le nom du dépôt interne ${data.internalRepoName} existe déjà en base pour ce projet`, - ); - const dbData = { - ...data, - isInfra: !!data.isInfra, - isPrivate: !!data.isPrivate, - }; - delete dbData.externalToken; + if (project.repositories?.find(repo => repo.internalRepoName === data.internalRepoName)) return new BadRequest400(`Le nom du dépôt interne ${data.internalRepoName} existe déjà en base pour ce projet`) + const dbData = { ...data, isInfra: !!data.isInfra, isPrivate: !!data.isPrivate } + delete dbData.externalToken - const repo = await initializeRepository(dbData); - const { results } = await hook.project.upsert( - project.id, - data.isPrivate - ? { - [repo.internalRepoName]: { - token: data.externalToken ?? '', - username: data.externalUserName ?? '', - }, - } - : undefined, - ); - await addLogs({ - action: 'Create Repository', - data: results, - userId, - requestId, - projectId: repo.projectId, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services lors de la création du dépôt', - ); - } + const repo = await initializeRepository(dbData) + const { results } = await hook.project.upsert(project.id, data.isPrivate + ? { + [repo.internalRepoName]: { + token: data.externalToken ?? '', + username: data.externalUserName ?? '', + }, + } + : undefined) + await addLogs({ action: 'Create Repository', data: results, userId, requestId, projectId: repo.projectId }) + if (results.failed) { + return new Unprocessable422('Echec des services lors de la création du dépôt') + } - if (data.externalRepoUrl) { - await syncRepository({ - repositoryId: repo.id, - requestId, - syncAllBranches: true, - userId, - }); - } - return repo; + if (data.externalRepoUrl) { + await syncRepository({ repositoryId: repo.id, requestId, syncAllBranches: true, userId }) + } + return repo } export async function updateRepository({ - repositoryId, - data, - userId, - requestId, + repositoryId, + data, + userId, + requestId, }: { - repositoryId: Repository['id']; - data: Partial; - userId: User['id']; - requestId: string; + repositoryId: Repository['id'] + data: Partial + userId: User['id'] + requestId: string }) { - const dbData = { ...data }; - delete dbData.externalToken; - const repo = await updateRepositoryQuery(repositoryId, dbData); + const dbData = { ...data } + delete dbData.externalToken + const repo = await updateRepositoryQuery(repositoryId, dbData) - const { results } = await hook.project.upsert(repo.projectId, { - [repo.internalRepoName]: { - username: repo.externalUserName ?? '', - token: data.externalToken ?? '', - }, - }); - await addLogs({ - action: 'Update Repository', - data: results, - userId, - requestId, - projectId: repo.projectId, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services à la mise à jour du dépôt', - ); - } + const { results } = await hook.project.upsert(repo.projectId, { + [repo.internalRepoName]: { + username: repo.externalUserName ?? '', + token: data.externalToken ?? '', + }, + }) + await addLogs({ action: 'Update Repository', data: results, userId, requestId, projectId: repo.projectId }) + if (results.failed) { + return new Unprocessable422('Echec des services à la mise à jour du dépôt') + } - return repo; + return repo } export async function deleteRepository({ - repositoryId, - userId, - requestId, - projectId, + repositoryId, + userId, + requestId, + projectId, }: { - repositoryId: Repository['id']; - userId: User['id']; - requestId: string; - projectId: Project['id']; + repositoryId: Repository['id'] + userId: User['id'] + requestId: string + projectId: Project['id'] }) { - await deleteRepositoryQuery(repositoryId); + await deleteRepositoryQuery(repositoryId) - const { results } = await hook.project.upsert(projectId); - await addLogs({ - action: 'Delete Repository', - data: results, - userId, - requestId, - projectId, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services à la suppression du dépôt', - ); - } - return null; + const { results } = await hook.project.upsert(projectId) + await addLogs({ action: 'Delete Repository', data: results, userId, requestId, projectId }) + if (results.failed) { + return new Unprocessable422('Echec des services à la suppression du dépôt') + } + return null } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts index a7e54ee95..ec861cb69 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts @@ -1,81 +1,62 @@ -import prisma from '@old-server/prisma'; -import type { Project, Repository } from '@prisma/client'; +import type { Project, Repository } from '@prisma/client' +import prisma from '@old-server/prisma' // SELECT export function getRepositoryById(id: Repository['id']) { - return prisma.repository.findUniqueOrThrow({ where: { id } }); + return prisma.repository.findUniqueOrThrow({ where: { id } }) } export function getProjectRepositories(projectId: Project['id']) { - return prisma.repository.findMany({ where: { projectId } }); + return prisma.repository.findMany({ where: { projectId } }) } // CREATE -type RepositoryCreate = Pick< - Repository, - 'projectId' | 'internalRepoName' | 'isInfra' | 'isPrivate' -> & - Partial>; - -export function initializeRepository({ - projectId, - internalRepoName, - externalRepoUrl, - isInfra, - isPrivate, - externalUserName, -}: RepositoryCreate) { - return prisma.repository.create({ - data: { - projectId, - internalRepoName, - externalRepoUrl, - externalUserName, - isInfra, - isPrivate, - }, - }); +type RepositoryCreate = Pick & + Partial> + +export function initializeRepository({ projectId, internalRepoName, externalRepoUrl, isInfra, isPrivate, externalUserName, deployRevision, deployPath, helmValuesFiles }: RepositoryCreate) { + return prisma.repository.create({ + data: { + projectId, + internalRepoName, + externalRepoUrl, + externalUserName, + isInfra, + isPrivate, + deployRevision, + deployPath, + helmValuesFiles, + }, + }) } export function getHookRepository(id: Repository['id']) { - return prisma.repository.findUniqueOrThrow({ - where: { - id, - }, - include: { - project: true, - }, - }); + return prisma.repository.findUniqueOrThrow({ + where: { + id, + }, + include: { + project: true, + }, + }) } // UPDATE -export function updateRepository( - id: Repository['id'], - infos: Partial, -) { - return prisma.repository.update({ where: { id }, data: { ...infos } }); +export function updateRepository(id: Repository['id'], infos: Partial) { + return prisma.repository.update({ where: { id }, data: { ...infos } }) } // DELETE export async function deleteRepository(id: Repository['id']) { - const doesRepoExist = await getRepositoryById(id); - if (!doesRepoExist) - throw new Error( - "Le dépôt interne demandé n'existe pas en base pour ce projet", - ); - return prisma.repository.delete({ where: { id } }); + const doesRepoExist = await getRepositoryById(id) + if (!doesRepoExist) throw new Error('Le dépôt interne demandé n\'existe pas en base pour ce projet') + return prisma.repository.delete({ where: { id } }) } export function deleteAllRepositoryForProject(id: Project['id']) { - return prisma.repository.deleteMany({ where: { projectId: id } }); + return prisma.repository.deleteMany({ where: { projectId: id } }) } -export function _createRepository( - data: Parameters[0]['create'], -) { - return prisma.repository.upsert({ - create: data, - update: data, - where: { id: data.id }, - }); +export function _createRepository(data: Parameters[0]['create']) { + return prisma.repository.upsert({ create: data, update: data, where: { id: data.id } }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts index eaae97830..a85a139d4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts @@ -1,646 +1,402 @@ -import { PROJECT_PERMS, repositoryContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { - atDates, - getProjectMockInfos, - getUserMockInfos, -} from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessCreateMock = vi.spyOn(business, 'createRepository'); -const businessUpdateMock = vi.spyOn(business, 'updateRepository'); -const businessDeleteMock = vi.spyOn(business, 'deleteRepository'); -const businessSyncMock = vi.spyOn(business, 'syncRepository'); -const businessGetProjectRepositoriesMock = vi.spyOn( - business, - 'getProjectRepositories', -); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PROJECT_PERMS, repositoryContract } from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { atDates, getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' +import { BadRequest400 } from '../../utils/errors' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessCreateMock = vi.spyOn(business, 'createRepository') +const businessUpdateMock = vi.spyOn(business, 'updateRepository') +const businessDeleteMock = vi.spyOn(business, 'deleteRepository') +const businessSyncMock = vi.spyOn(business, 'syncRepository') +const businessGetProjectRepositoriesMock = vi.spyOn(business, 'getProjectRepositories') describe('repositoryRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - const projectId = faker.string.uuid(); - const repositoryId = faker.string.uuid(); - const repositoryData = { - projectId, - externalRepoUrl: `${faker.internet.url()}.git`, - isPrivate: true, - externalToken: faker.string.alpha(), - externalUserName: faker.internet.username(), - isInfra: false, - internalRepoName: faker.string.alpha({ length: 5, casing: 'lower' }), - }; - - describe('listRepositories', () => { - it('should return repositories for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessGetProjectRepositoriesMock.mockResolvedValueOnce([]); - - const response = await app - .inject() - .get(repositoryContract.listRepositories.path) - .query({ projectId }) - .end(); - - expect(businessGetProjectRepositoriesMock).toHaveBeenCalledWith( - projectId, - ); - expect(response.json()).toEqual([]); - expect(response.statusCode).toEqual(200); - }); - - it('should return empty for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.REPLAY_HOOKS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(repositoryContract.listRepositories.path) - .query({ projectId }) - .end(); - - expect(businessGetProjectRepositoriesMock).toHaveBeenCalledTimes(0); - expect(response.json()).toEqual([]); - }); - }); - - describe('syncRepository', () => { - it('should synchronize repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessSyncMock.mockResolvedValueOnce(null); - - const response = await app - .inject() - .post( - repositoryContract.syncRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body({ branchName: 'main', syncAllBranches: false }) - .end(); - - expect(response.statusCode).toEqual(204); - expect(businessSyncMock).toHaveBeenCalledWith({ - repositoryId, - userId: user.user.id, - branchName: 'main', - requestId: expect.any(String), - syncAllBranches: false, - }); - }); - - it('should return 403 for forbidden sync attempt', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.SEE_SECRETS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - repositoryContract.syncRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body({ branchName: 'main', syncAllBranches: false }) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - repositoryContract.syncRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body({ branchName: 'main', syncAllBranches: false }) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post( - repositoryContract.syncRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body({ branchName: 'main', syncAllBranches: false }) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessSyncMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .post( - repositoryContract.syncRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body({ branchName: 'main', syncAllBranches: false }) - .end(); - - expect(response.statusCode).toEqual(400); - }); - }); - - describe('createRepository', () => { - it('should create repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce({ - id: repositoryId, - ...repositoryData, - ...atDates, - }); - const response = await app - .inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end(); - - expect(response.statusCode).toEqual(201); - expect(response.json()).toMatchObject({ - id: repositoryId, - ...repositoryData, - }); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end(); - - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - expect(response.statusCode).toEqual(403); - }); - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end(); - - expect(response.statusCode).toEqual(404); - }); - it('should return 403 for insuficient permissions', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end(); - - expect(response.statusCode).toEqual(403); - }); - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end(); - - expect(response.statusCode).toEqual(400); - }); - }); - - describe('updateRepository', () => { - const repoUpdateData = { isInfra: true }; - it('should update repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce({ - id: repositoryId, - ...repositoryData, - ...repoUpdateData, - ...atDates, - }); - const response = await app - .inject() - .put( - repositoryContract.updateRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body(repoUpdateData) - .end(); - - expect(response.statusCode).toEqual(200); - expect(response.json()).toMatchObject({ - id: repositoryId, - ...repositoryData, - ...repoUpdateData, - }); - }); - - it('should update repository and drop creds if is not private', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const repoUpdateData = { - isPrivate: false, - externalUserName: 'test', - }; - businessUpdateMock.mockResolvedValueOnce({ - id: repositoryId, - ...repositoryData, - ...repoUpdateData, - ...atDates, - }); - const response = await app - .inject() - .put( - repositoryContract.updateRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body(repoUpdateData) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledWith({ - data: { isPrivate: false }, - repositoryId, - requestId: expect.any(String), - userId: user.user.id, - }); - expect(response.json()).toMatchObject({ - id: repositoryId, - ...repositoryData, - ...repoUpdateData, - }); - expect(response.statusCode).toEqual(200); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectLocked: true }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - repositoryContract.updateRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body(repoUpdateData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if not enough permissions', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - repositoryContract.updateRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body(repoUpdateData) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - repositoryContract.updateRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body(repoUpdateData) - .end(); - - expect(response.statusCode).toEqual(404); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - repositoryContract.updateRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body(repoUpdateData) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - }); - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .put( - repositoryContract.updateRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .body(repoUpdateData) - .end(); - - expect(response.statusCode).toEqual(400); - }); - // TODO add tests about filtering - }); - - describe('deleteRepository', () => { - it('should delete repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce(null); - const response = await app - .inject() - .delete( - repositoryContract.deleteRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(204); - }); - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - projectLocked: true, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - repositoryContract.deleteRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(403); - expect(response.json()).toEqual({ - message: 'Le projet est verrouillé', - }); - }); - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - projectStatus: 'archived', - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - repositoryContract.deleteRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .end(); - - expect(response.json()).toEqual({ - message: 'Le projet est archivé', - }); - expect(response.statusCode).toEqual(403); - }); - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: 0n, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - repositoryContract.deleteRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(404); - }); - it('should return 403 if not enough privilege', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - repositoryContract.deleteRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(403); - }); - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ - projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, - }); - const user = getUserMockInfos(false, undefined, projectPerms); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .delete( - repositoryContract.deleteRepository.path.replace( - ':repositoryId', - repositoryId, - ), - ) - .end(); - - expect(response.statusCode).toEqual(400); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + const projectId = faker.string.uuid() + const repositoryId = faker.string.uuid() + const repositoryData = { + projectId, + externalRepoUrl: `${faker.internet.url()}.git`, + isPrivate: true, + externalToken: faker.string.alpha(), + externalUserName: faker.internet.username(), + isInfra: false, + internalRepoName: faker.string.alpha({ length: 5, casing: 'lower' }), + } + + describe('listRepositories', () => { + it('should return repositories for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessGetProjectRepositoriesMock.mockResolvedValueOnce([]) + + const response = await app.inject() + .get(repositoryContract.listRepositories.path) + .query({ projectId }) + .end() + + expect(businessGetProjectRepositoriesMock).toHaveBeenCalledWith(projectId) + expect(response.json()).toEqual([]) + expect(response.statusCode).toEqual(200) + }) + + it('should return empty for unauthorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.REPLAY_HOOKS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(repositoryContract.listRepositories.path) + .query({ projectId }) + .end() + + expect(businessGetProjectRepositoriesMock).toHaveBeenCalledTimes(0) + expect(response.json()).toEqual([]) + }) + }) + + describe('syncRepository', () => { + it('should synchronize repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(null) + + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(204) + expect(businessSyncMock).toHaveBeenCalledWith({ repositoryId, userId: user.user.id, branchName: 'main', requestId: expect.any(String), syncAllBranches: false }) + }) + + it('should return 403 for forbidden sync attempt', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 403 for archived project', async () => { + const projectPerms = getProjectMockInfos({ projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessSyncMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) + .body({ branchName: 'main', syncAllBranches: false }) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('createRepository', () => { + it('should create repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...atDates }) + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(201) + expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData }) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(404) + }) + it('should return 403 for insuficient permissions', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(403) + }) + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(repositoryContract.createRepository.path) + .body(repositoryData) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) + + describe('updateRepository', () => { + const repoUpdateData = { isInfra: true } + it('should update repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...repoUpdateData, ...atDates }) + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(200) + expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData, ...repoUpdateData }) + }) + + it('should update repository and drop creds if is not private', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const repoUpdateData = { isPrivate: false, externalUserName: 'test' } + businessUpdateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...repoUpdateData, ...atDates }) + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(businessUpdateMock).toHaveBeenCalledWith({ data: { isPrivate: false }, repositoryId, requestId: expect.any(String), userId: user.user.id }) + expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData, ...repoUpdateData }) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if not enough permissions', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 if non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(404) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + }) + + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) + .body(repoUpdateData) + .end() + + expect(response.statusCode).toEqual(400) + }) + // TODO add tests about filtering + }) + + describe('deleteRepository', () => { + it('should delete repository for authorized user', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 if project is locked', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectLocked: true }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(403) + expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) + }) + + it('should return 403 if project is archived', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectStatus: 'archived' }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.json()).toEqual({ message: 'Le projet est archivé' }) + expect(response.statusCode).toEqual(403) + }) + + it('should return 404 for non-member', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(404) + }) + it('should return 403 if not enough privilege', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(403) + }) + it('should pass business error', async () => { + const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) + const user = getUserMockInfos(false, undefined, projectPerms) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) + .end() + + expect(response.statusCode).toEqual(400) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts index dabdfaf55..4ab014e1b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts @@ -1,197 +1,138 @@ +import { AdminAuthorized, ProjectAuthorized, fakeToken, repositoryContract } from '@cpn-console/shared' import { - AdminAuthorized, - ProjectAuthorized, - fakeToken, - repositoryContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - NotFound404, - Unauthorized401, -} from '@old-server/utils/errors'; -import { filterObjectByKeys } from '@old-server/utils/queries-tools'; - -import { - createRepository, - deleteRepository, - getProjectRepositories, - syncRepository, - updateRepository, -} from './business'; - -@Injectable() -export class RepositoryRouterService { - constructor(private readonly appService: AppService) {} - - repositoryRouter() { - return this.appService.serverInstance.router(repositoryContract, { - // Récupérer tous les repositories d'un projet - listRepositories: async ({ request: req, query }) => { - const projectId = query.projectId; - const perms = await authUser(req, { id: projectId }); - - const body = ProjectAuthorized.ListRepositories(perms) - ? await getProjectRepositories(projectId) - : []; - - return { - status: 200, - body, - }; - }, - - // Synchroniser un repository - syncRepository: async ({ request: req, params, body }) => { - const { repositoryId } = params; - const perms = await authUser(req, { repositoryId }); - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const { syncAllBranches, branchName } = body; - - const resBody = await syncRepository({ - repositoryId, - userId: perms.user.id, - branchName, - requestId: req.id, - syncAllBranches, - }); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 204, - body: resBody, - }; - }, - - // Créer un repository - createRepository: async ({ request: req, body: data }) => { - const projectId = data.projectId; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await createRepository({ - data, - userId: perms.user.id, - requestId: req.id, - }); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Mettre à jour un repository - updateRepository: async ({ request: req, params, body }) => { - const repositoryId = params.repositoryId; - const perms = await authUser(req, { repositoryId }); - - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) - return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const keysAllowedForUpdate = [ - 'externalRepoUrl', - 'isPrivate', - 'externalToken', - 'externalUserName', - 'isInfra', - ]; - const data = filterObjectByKeys(body, keysAllowedForUpdate); - - if (data.externalToken === fakeToken) { - delete data.externalToken; - } - - if (data.isPrivate === false) { - delete data.externalToken; - delete data.externalUserName; - } - - const resBody = await updateRepository({ - repositoryId, - data, - userId: perms.user.id, - requestId: req.id, - }); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - // Supprimer un repository - deleteRepository: async ({ request: req, params }) => { - const repositoryId = params.repositoryId; - const perms = await authUser(req, { repositoryId }); - - if (!perms.user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await deleteRepository({ - repositoryId, - userId: perms.user.id, - requestId: req.id, - projectId: perms.projectId, - }); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); - } + createRepository, + deleteRepository, + getProjectRepositories, + syncRepository, + updateRepository, +} from './business' +import { serverInstance } from '@old-server/app' + +import { filterObjectByKeys } from '@old-server/utils/queries-tools' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' + +export function repositoryRouter() { + return serverInstance.router(repositoryContract, { + // Récupérer tous les repositories d'un projet + listRepositories: async ({ request: req, query }) => { + const projectId = query.projectId + const perms = await authUser(req, { id: projectId }) + + const body = ProjectAuthorized.ListRepositories(perms) + ? await getProjectRepositories(projectId) + : [] + + return { + status: 200, + body, + } + }, + + // Synchroniser un repository + syncRepository: async ({ request: req, params, body }) => { + const { repositoryId } = params + const perms = await authUser(req, { repositoryId }) + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const { syncAllBranches, branchName } = body + + const resBody = await syncRepository({ repositoryId, userId: perms.user.id, branchName, requestId: req.id, syncAllBranches }) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 204, + body: resBody, + } + }, + + // Créer un repository + createRepository: async ({ request: req, body: data }) => { + const projectId = data.projectId + const perms = await authUser(req, { id: projectId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const body = await createRepository({ data, userId: perms.user.id, requestId: req.id }) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + // Mettre à jour un repository + updateRepository: async ({ request: req, params, body }) => { + const repositoryId = params.repositoryId + const perms = await authUser(req, { repositoryId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const keysAllowedForUpdate = [ + 'externalRepoUrl', + 'isPrivate', + 'externalToken', + 'externalUserName', + 'isInfra', + 'deployRevision', + 'deployPath', + 'helmValuesFiles', + ] + const data = filterObjectByKeys(body, keysAllowedForUpdate) + + if (data.externalToken === fakeToken) { + delete data.externalToken + } + + if (data.isPrivate === false) { + delete data.externalToken + delete data.externalUserName + } + + const resBody = await updateRepository({ repositoryId, data, userId: perms.user.id, requestId: req.id }) + if (resBody instanceof ErrorResType) return resBody + + return { + status: 200, + body: resBody, + } + }, + + // Supprimer un repository + deleteRepository: async ({ request: req, params }) => { + const repositoryId = params.repositoryId + const perms = await authUser(req, { repositoryId }) + + if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + if (!perms.projectPermissions) return new NotFound404() + if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + const body = await deleteRepository({ + repositoryId, + userId: perms.user.id, + requestId: req.id, + projectId: perms.projectId, + }) + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts index 558fe4b1d..1b08e2cc4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts @@ -1,174 +1,171 @@ import type { - ServiceChain, - ServiceChainDetails, - ServiceChainFlows, -} from '@cpn-console/shared'; + ServiceChain, + ServiceChainDetails, + ServiceChainFlows, +} from '@cpn-console/shared' import { - serviceChainEnvironmentEnum, - serviceChainFlowStateEnum, - serviceChainLocationEnum, - serviceChainNetworkEnum, - serviceChainStateEnum, -} from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import axios from 'axios'; -import type { Mock } from 'vitest'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + serviceChainEnvironmentEnum, + serviceChainFlowStateEnum, + serviceChainLocationEnum, + serviceChainNetworkEnum, + serviceChainStateEnum, +} from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import axios from 'axios' +import type { Mock } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { - getServiceChainDetails, - getServiceChainFlows, - listServiceChains, - retryServiceChain, - validateServiceChain, -} from './business'; + getServiceChainDetails, + getServiceChainFlows, + listServiceChains, + retryServiceChain, + validateServiceChain, +} from './business' -vi.mock('axios'); +vi.mock('axios') -let serviceChain: ServiceChain; -let serviceChainDetails: ServiceChainDetails; -let serviceChainFlows: ServiceChainFlows; +let serviceChain: ServiceChain +let serviceChainDetails: ServiceChainDetails +let serviceChainFlows: ServiceChainFlows describe('test ServiceChain business logic', () => { - beforeEach(() => { - serviceChain = { - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement(serviceChainNetworkEnum), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - }; - - serviceChainDetails = { - ...serviceChain, - validationId: faker.string.uuid(), - validatedBy: faker.helpers.maybe(() => faker.string.uuid()) || null, - ref: faker.string.uuid(), - location: faker.helpers.arrayElement(serviceChainLocationEnum), - targetAddress: faker.internet.ipv4(), - projectId: faker.string.uuid(), - env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), - subjectAlternativeName: faker.helpers.uniqueArray( - faker.internet.domainName, - 3, - ), - redirect: faker.datatype.boolean(), - antivirus: - faker.helpers.maybe(() => ({ - maxFileSize: faker.number.int(), - })) || null, // undefined is not wanted here - websocket: faker.datatype.boolean(), - ipWhiteList: faker.helpers - .uniqueArray(faker.internet.ipv4, 5) - .map((e) => `${e}/32`), // We want a CIDR here - sslOutgoing: faker.datatype.boolean(), - }; - - serviceChainFlows = { - reserve_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - create_cert: - faker.helpers.maybe(() => ({ - state: faker.helpers.arrayElement( - serviceChainFlowStateEnum, - ), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - })) || null, - call_exec: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - activate_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - dns_request: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - }; - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('listServiceChains', () => { - it('should return a list of service chains', async () => { - const input = [serviceChain]; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }); - - const result = await listServiceChains(); - - expect(result).toStrictEqual(input); - }); - }); - - describe('getServiceChainDetails', () => { - it('should return a service chain details', async () => { - const input = serviceChainDetails; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }); - - const result = await getServiceChainDetails(faker.string.uuid()); - - expect(result).toStrictEqual(input); - }); - }); - - describe('retryServiceChain', () => { - it('should trigger a service chain retry attempt', async () => { - const input = {}; - (axios.create as Mock).mockReturnValue({ - post: () => ({ data: input }), - }); - - const result = await retryServiceChain(faker.string.uuid()); - - expect(result.data).toStrictEqual(input); - }); - }); - - describe('validateServiceChain', () => { - it('should trigger a service chain validate attempt', async () => { - const input = {}; - (axios.create as Mock).mockReturnValue({ - post: () => ({ data: input }), - }); - - const result = await validateServiceChain(faker.string.uuid()); - - expect(result.data).toStrictEqual(input); - }); - }); - - describe('getServiceChainFlows', () => { - it('should return a service chain flows', async () => { - const input = serviceChainFlows; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }); - - const result = await getServiceChainFlows(faker.string.uuid()); - - expect(result).toStrictEqual(input); - }); - }); -}); + beforeEach(() => { + serviceChain = { + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement(serviceChainNetworkEnum), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + } + + serviceChainDetails = { + ...serviceChain, + validationId: faker.string.uuid(), + validatedBy: faker.helpers.maybe(() => faker.string.uuid()) || null, + ref: faker.string.uuid(), + location: faker.helpers.arrayElement(serviceChainLocationEnum), + targetAddress: faker.internet.ipv4(), + projectId: faker.string.uuid(), + env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), + subjectAlternativeName: faker.helpers.uniqueArray( + faker.internet.domainName, + 3, + ), + redirect: faker.datatype.boolean(), + antivirus: + faker.helpers.maybe(() => ({ + maxFileSize: faker.number.int(), + })) || null, // undefined is not wanted here + websocket: faker.datatype.boolean(), + ipWhiteList: faker.helpers + .uniqueArray(faker.internet.ipv4, 5) + .map(e => `${e}/32`), // We want a CIDR here + sslOutgoing: faker.datatype.boolean(), + } + + serviceChainFlows = { + reserve_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + create_cert: faker.helpers.maybe(() => ({ + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + })) || null, + call_exec: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + activate_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + dns_request: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + } + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('listServiceChains', () => { + it('should return a list of service chains', async () => { + const input = [serviceChain]; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }) + + const result = await listServiceChains() + + expect(result).toStrictEqual(input) + }) + }) + + describe('getServiceChainDetails', () => { + it('should return a service chain details', async () => { + const input = serviceChainDetails; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }) + + const result = await getServiceChainDetails(faker.string.uuid()) + + expect(result).toStrictEqual(input) + }) + }) + + describe('retryServiceChain', () => { + it('should trigger a service chain retry attempt', async () => { + const input = {}; + (axios.create as Mock).mockReturnValue({ + post: () => ({ data: input }), + }) + + const result = await retryServiceChain(faker.string.uuid()) + + expect(result.data).toStrictEqual(input) + }) + }) + + describe('validateServiceChain', () => { + it('should trigger a service chain validate attempt', async () => { + const input = {}; + (axios.create as Mock).mockReturnValue({ + post: () => ({ data: input }), + }) + + const result = await validateServiceChain(faker.string.uuid()) + + expect(result.data).toStrictEqual(input) + }) + }) + + describe('getServiceChainFlows', () => { + it('should return a service chain flows', async () => { + const input = serviceChainFlows; + (axios.create as Mock).mockReturnValue({ + get: () => ({ data: input }), + }) + + const result = await getServiceChainFlows(faker.string.uuid()) + + expect(result).toStrictEqual(input) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts index 35aa603ef..a63755604 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts @@ -1,27 +1,27 @@ import { - getServiceChainDetails as getServiceChainDetailsQuery, - getServiceChainFlows as getServiceChainFlowsQuery, - listServiceChains as listServiceChainsQuery, - retryServiceChain as retryServiceChainQuery, - validateServiceChain as validateServiceChainQuery, -} from '@old-server/resources/queries-index'; + getServiceChainDetails as getServiceChainDetailsQuery, + listServiceChains as listServiceChainsQuery, + retryServiceChain as retryServiceChainQuery, + validateServiceChain as validateServiceChainQuery, + getServiceChainFlows as getServiceChainFlowsQuery, +} from '@old-server/resources/queries-index' export async function listServiceChains() { - return listServiceChainsQuery(); + return listServiceChainsQuery() } export async function getServiceChainDetails(serviceChainId: string) { - return getServiceChainDetailsQuery(serviceChainId); + return getServiceChainDetailsQuery(serviceChainId) } export async function retryServiceChain(serviceChainId: string) { - return retryServiceChainQuery(serviceChainId); + return retryServiceChainQuery(serviceChainId) } export async function validateServiceChain(validationId: string) { - return validateServiceChainQuery(validationId); + return validateServiceChainQuery(validationId) } export async function getServiceChainFlows(serviceChainId: string) { - return getServiceChainFlowsQuery(serviceChainId); + return getServiceChainFlowsQuery(serviceChainId) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts index 6ffbe5f77..10713007c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts @@ -1,58 +1,58 @@ import { - type ServiceChain, - ServiceChainDetailsSchema, - ServiceChainFlowsSchema, - ServiceChainListSchema, -} from '@cpn-console/shared'; -import axios from 'axios'; -import https from 'node:https'; + type ServiceChain, + ServiceChainDetailsSchema, + ServiceChainFlowsSchema, + ServiceChainListSchema, +} from '@cpn-console/shared' +import axios from 'axios' +import https from 'node:https' -const openCDSEnvVar = 'OPENCDS_URL'; -const openCDSTargetURL = process.env[openCDSEnvVar]; -const openCDSDisabledErrorMessage = `OpenCDS is disabled, please set ${openCDSEnvVar} in your relevant .env file. See .env-example`; +const openCDSEnvVar = 'OPENCDS_URL' +const openCDSTargetURL = process.env[openCDSEnvVar] +const openCDSDisabledErrorMessage = `OpenCDS is disabled, please set ${openCDSEnvVar} in your relevant .env file. See .env-example` function getClient() { - if (!openCDSTargetURL) { - throw new Error(openCDSDisabledErrorMessage); - } - return axios.create({ - baseURL: openCDSTargetURL, - httpsAgent: new https.Agent({ - rejectUnauthorized: - // We want it to be `false` only if it has explicitly - // been stated as "false" in the env vars - process.env.OPENCDS_API_TLS_REJECT_UNAUTHORIZED !== 'false', - }), - headers: { - 'X-API-Key': process.env.OPENCDS_API_TOKEN, - }, - }); + if (!openCDSTargetURL) { + throw new Error(openCDSDisabledErrorMessage) + } + return axios.create({ + baseURL: openCDSTargetURL, + httpsAgent: new https.Agent({ + rejectUnauthorized: + // We want it to be `false` only if it has explicitly + // been stated as "false" in the env vars + process.env.OPENCDS_API_TLS_REJECT_UNAUTHORIZED !== 'false', + }), + headers: { + 'X-API-Key': process.env.OPENCDS_API_TOKEN, + }, + }) } export async function listServiceChains() { - return ServiceChainListSchema.parse( - (await getClient().get(`/requests`)).data, - ); + return ServiceChainListSchema.parse( + (await getClient().get(`/requests`)).data, + ) } export async function getServiceChainDetails( - serviceChainId: ServiceChain['id'], + serviceChainId: ServiceChain['id'], ) { - return ServiceChainDetailsSchema.parse( - (await getClient().get(`/requests/${serviceChainId}`)).data, - ); + return ServiceChainDetailsSchema.parse( + (await getClient().get(`/requests/${serviceChainId}`)).data, + ) } export async function retryServiceChain(serviceChainId: ServiceChain['id']) { - return await getClient().post(`/requests/${serviceChainId}/retry`); + return await getClient().post(`/requests/${serviceChainId}/retry`) } export async function validateServiceChain(validationId: string) { - return await getClient().post(`/validate/${validationId}`); + return await getClient().post(`/validate/${validationId}`) } export async function getServiceChainFlows(serviceChainId: ServiceChain['id']) { - return ServiceChainFlowsSchema.parse( - (await getClient().get(`/requests/${serviceChainId}/flows`)).data, - ); + return ServiceChainFlowsSchema.parse( + (await getClient().get(`/requests/${serviceChainId}/flows`)).data, + ) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts index 61d60b78c..c6ee037f7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts @@ -1,340 +1,306 @@ -import type { - ServiceChain, - ServiceChainDetails, - ServiceChainFlows, -} from '@cpn-console/shared'; +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ServiceChain, ServiceChainDetails, ServiceChainFlows } from '@cpn-console/shared' import { - ServiceChainDetailsSchema, - ServiceChainFlowsSchema, - ServiceChainListSchema, - serviceChainContract, - serviceChainEnvironmentEnum, - serviceChainFlowStateEnum, - serviceChainLocationEnum, - serviceChainNetworkEnum, - serviceChainStateEnum, -} from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; + ServiceChainDetailsSchema, + ServiceChainFlowsSchema, + ServiceChainListSchema, + serviceChainContract, + serviceChainEnvironmentEnum, + serviceChainFlowStateEnum, + serviceChainLocationEnum, + serviceChainNetworkEnum, + serviceChainStateEnum, +} from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getUserMockInfos } from '../../utils/mocks' +import * as business from './business' vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessListServiceChainsMock = vi.spyOn(business, 'listServiceChains'); -const businessGetServiceChainDetailsMock = vi.spyOn( - business, - 'getServiceChainDetails', -); -const businessRetryServiceChainMock = vi.spyOn(business, 'retryServiceChain'); -const businessValidateServiceChainMock = vi.spyOn( - business, - 'validateServiceChain', -); -const businessGetServiceChainsFlowsMock = vi.spyOn( - business, - 'getServiceChainFlows', -); + 'fastify-keycloak-adapter', + (await import('../../utils/mocks')).mockSessionPlugin, +) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListServiceChainsMock = vi.spyOn(business, 'listServiceChains') +const businessGetServiceChainDetailsMock = vi.spyOn(business, 'getServiceChainDetails') +const businessRetryServiceChainMock = vi.spyOn(business, 'retryServiceChain') +const businessValidateServiceChainMock = vi.spyOn(business, 'validateServiceChain') +const businessGetServiceChainsFlowsMock = vi.spyOn(business, 'getServiceChainFlows') describe('test ServiceChainContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - describe('listServiceChains', () => { - it('as non admin', async () => { - const user = getUserMockInfos(false); + beforeEach(() => { + vi.resetAllMocks() + }) + describe('listServiceChains', () => { + it('as non admin', async () => { + const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user); + authUserMock.mockResolvedValueOnce(user) - businessListServiceChainsMock.mockResolvedValueOnce([]); - const response = await app - .inject() - .get(serviceChainContract.listServiceChains.path) - .end(); + businessListServiceChainsMock.mockResolvedValueOnce([]) + const response = await app + .inject() + .get(serviceChainContract.listServiceChains.path) + .end() - expect(response.json()).toStrictEqual([]); - expect(response.statusCode).toEqual(200); - }); - it('as admin', async () => { - const user = getUserMockInfos(true); - const serviceChainList = faker.helpers.multiple( - () => ({ - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement( - serviceChainNetworkEnum, - ), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - }), - ); + expect(response.json()).toStrictEqual([]) + expect(response.statusCode).toEqual(200) + }) + it('as admin', async () => { + const user = getUserMockInfos(true) + const serviceChainList = faker.helpers.multiple(() => ({ + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement(serviceChainNetworkEnum), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + })) - authUserMock.mockResolvedValueOnce(user); + authUserMock.mockResolvedValueOnce(user) - businessListServiceChainsMock.mockResolvedValueOnce( - serviceChainList, - ); - const response = await app - .inject() - .get(serviceChainContract.listServiceChains.path) - .end(); + businessListServiceChainsMock.mockResolvedValueOnce(serviceChainList) + const response = await app + .inject() + .get(serviceChainContract.listServiceChains.path) + .end() - expect(businessListServiceChainsMock).toHaveBeenCalledWith(); + expect(businessListServiceChainsMock).toHaveBeenCalledWith() - expect(ServiceChainListSchema.parse(response.json())).toStrictEqual( - serviceChainList, - ); - expect(response.statusCode).toEqual(200); - }); - }); + expect(ServiceChainListSchema.parse(response.json())).toStrictEqual( + serviceChainList, + ) + expect(response.statusCode).toEqual(200) + }) + }) - describe('getServiceChainDetails', () => { - it('should return serviceChain details', async () => { - const serviceChainDetails: ServiceChainDetails = { - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement(serviceChainNetworkEnum), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - validationId: faker.string.uuid(), - validatedBy: faker.string.uuid(), - ref: faker.string.uuid(), - location: faker.helpers.arrayElement(serviceChainLocationEnum), - targetAddress: faker.internet.ipv4(), - projectId: faker.string.uuid(), - env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), - subjectAlternativeName: faker.helpers.uniqueArray( - faker.internet.domainName, - 3, - ), - redirect: faker.datatype.boolean(), - antivirus: - faker.helpers.maybe(() => ({ - maxFileSize: faker.number.int(), - })) || null, // undefined is not wanted here - websocket: faker.datatype.boolean(), - ipWhiteList: faker.helpers - .uniqueArray(faker.internet.ipv4, 5) - .map((e) => `${e}/32`), // We want a CIDR here - sslOutgoing: faker.datatype.boolean(), - }; - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); + describe('getServiceChainDetails', () => { + it('should return serviceChain details', async () => { + const serviceChainDetails: ServiceChainDetails = { + id: faker.string.uuid(), + state: faker.helpers.arrayElement(serviceChainStateEnum), + commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, + pai: faker.string.alpha(3).toUpperCase(), + network: faker.helpers.arrayElement(serviceChainNetworkEnum), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), + validationId: faker.string.uuid(), + validatedBy: faker.string.uuid(), + ref: faker.string.uuid(), + location: faker.helpers.arrayElement(serviceChainLocationEnum), + targetAddress: faker.internet.ipv4(), + projectId: faker.string.uuid(), + env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), + subjectAlternativeName: faker.helpers.uniqueArray( + faker.internet.domainName, + 3, + ), + redirect: faker.datatype.boolean(), + antivirus: + faker.helpers.maybe(() => ({ + maxFileSize: faker.number.int(), + })) || null, // undefined is not wanted here + websocket: faker.datatype.boolean(), + ipWhiteList: faker.helpers + .uniqueArray(faker.internet.ipv4, 5) + .map(e => `${e}/32`), // We want a CIDR here + sslOutgoing: faker.datatype.boolean(), + } + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) - businessGetServiceChainDetailsMock.mockResolvedValueOnce( - serviceChainDetails, - ); - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainDetails.path.replace( - ':serviceChainId', - serviceChainDetails.id, - ), - ) - .end(); + businessGetServiceChainDetailsMock.mockResolvedValueOnce(serviceChainDetails) + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainDetails.path.replace( + ':serviceChainId', + serviceChainDetails.id, + ), + ) + .end() - expect(ServiceChainDetailsSchema.parse(response.json())).toEqual( - serviceChainDetails, - ); - expect(response.statusCode).toEqual(200); - expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(1); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); + expect(ServiceChainDetailsSchema.parse(response.json())).toEqual( + serviceChainDetails, + ) + expect(response.statusCode).toEqual(200) + expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(1) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainDetails.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end(); + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainDetails.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() - expect(response.statusCode).toEqual(403); - expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(0); - }); - }); + expect(response.statusCode).toEqual(403) + expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(0) + }) + }) - describe('retryServiceChain', () => { - it('should return 204', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); + describe('retryServiceChain', () => { + it('should return 204', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) - businessRetryServiceChainMock.mockResolvedValueOnce({ - status: 204, - body: undefined, - }); - const response = await app - .inject() - .post( - serviceChainContract.retryServiceChain.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end(); + businessRetryServiceChainMock.mockResolvedValueOnce({ + status: 204, + body: undefined, + }) + const response = await app + .inject() + .post( + serviceChainContract.retryServiceChain.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() - expect(response.body).toEqual(''); - expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(1); - expect(response.statusCode).toEqual(204); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); + expect(response.body).toEqual('') + expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(1) + expect(response.statusCode).toEqual(204) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) - const response = await app - .inject() - .post( - serviceChainContract.retryServiceChain.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end(); + const response = await app + .inject() + .post( + serviceChainContract.retryServiceChain.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() - expect(response.statusCode).toEqual(403); - expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(0); - }); - }); + expect(response.statusCode).toEqual(403) + expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(0) + }) + }) - describe('validateServiceChain', () => { - it('should return 204', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); + describe('validateServiceChain', () => { + it('should return 204', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) - businessValidateServiceChainMock.mockResolvedValueOnce({ - status: 204, - body: undefined, - }); - const response = await app - .inject() - .post( - serviceChainContract.validateServiceChain.path.replace( - ':validationId', - faker.string.uuid(), - ), - ) - .end(); + businessValidateServiceChainMock.mockResolvedValueOnce({ + status: 204, + body: undefined, + }) + const response = await app + .inject() + .post( + serviceChainContract.validateServiceChain.path.replace( + ':validationId', + faker.string.uuid(), + ), + ) + .end() - expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(1); - expect(response.body).toEqual(''); - expect(response.statusCode).toEqual(204); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); + expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(1) + expect(response.body).toEqual('') + expect(response.statusCode).toEqual(204) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) - const response = await app - .inject() - .post( - serviceChainContract.validateServiceChain.path.replace( - ':validationId', - faker.string.uuid(), - ), - ) - .end(); + const response = await app + .inject() + .post( + serviceChainContract.validateServiceChain.path.replace( + ':validationId', + faker.string.uuid(), + ), + ) + .end() - expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); + expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) - describe('getServiceChainFlows', () => { - it('should return serviceChain flows', async () => { - const serviceChainFlows: ServiceChainFlows = { - reserve_ip: { - state: faker.helpers.arrayElement( - serviceChainFlowStateEnum, - ), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - create_cert: { - state: faker.helpers.arrayElement( - serviceChainFlowStateEnum, - ), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - call_exec: { - state: faker.helpers.arrayElement( - serviceChainFlowStateEnum, - ), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - activate_ip: { - state: faker.helpers.arrayElement( - serviceChainFlowStateEnum, - ), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - dns_request: { - state: faker.helpers.arrayElement( - serviceChainFlowStateEnum, - ), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - }; - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); + describe('getServiceChainFlows', () => { + it('should return serviceChain flows', async () => { + const serviceChainFlows: ServiceChainFlows = { + reserve_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + create_cert: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + call_exec: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + activate_ip: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + dns_request: { + state: faker.helpers.arrayElement(serviceChainFlowStateEnum), + input: '{ "foo": 0, "bar": true, "qux": "test" }', + output: '{ "foo": 0, "bar": true, "qux": "test" }', + updatedAt: faker.date.recent(), + }, + } + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) - businessGetServiceChainsFlowsMock.mockResolvedValueOnce( - serviceChainFlows, - ); - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainFlows.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end(); + businessGetServiceChainsFlowsMock.mockResolvedValueOnce(serviceChainFlows) + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainFlows.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() - expect(ServiceChainFlowsSchema.parse(response.json())).toEqual( - serviceChainFlows, - ); - expect(response.statusCode).toEqual(200); - expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(1); - }); - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); + expect(ServiceChainFlowsSchema.parse(response.json())).toEqual( + serviceChainFlows, + ) + expect(response.statusCode).toEqual(200) + expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(1) + }) + it('should return 403 if not admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainFlows.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end(); + const response = await app + .inject() + .get( + serviceChainContract.getServiceChainFlows.path.replace( + ':serviceChainId', + faker.string.uuid(), + ), + ) + .end() - expect(response.statusCode).toEqual(403); - expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(0); - }); - }); -}); + expect(response.statusCode).toEqual(403) + expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts index c64a4fc94..41289f4b9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts @@ -1,97 +1,90 @@ -import type { AsyncReturnType } from '@cpn-console/shared'; -import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import '@old-server/types/index'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403 } from '@old-server/utils/errors'; - +import type { AsyncReturnType } from '@cpn-console/shared' +import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared' import { - getServiceChainDetails as getServiceChainDetailsBusiness, - getServiceChainFlows as getServiceChainFlowsBusiness, - listServiceChains as listServiceChainsBusiness, - retryServiceChain as retryServiceChainBusiness, - validateServiceChain as validateServiceChainBusiness, -} from './business'; - -@Injectable() -export class ServiceChainRouterService { - constructor(private readonly appService: AppService) {} - - serviceChainRouter() { - return this.appService.serverInstance.router(serviceChainContract, { - listServiceChains: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - - let body: AsyncReturnType = - []; - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listServiceChainsBusiness(); - } - - return { - status: 200, - body, - }; - }, - - getServiceChainDetails: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - const serviceChainDetails = - await getServiceChainDetailsBusiness(serviceChainId); - - return { - status: 200, - body: serviceChainDetails, - }; - }, - - retryServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - await retryServiceChainBusiness(serviceChainId); - - return { - status: 204, - body: null, - }; - }, - - validateServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.validationId; - await validateServiceChainBusiness(serviceChainId); - - return { - status: 204, - body: null, - }; - }, - - getServiceChainFlows: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - const serviceChainFlows = - await getServiceChainFlowsBusiness(serviceChainId); - - return { - status: 200, - body: serviceChainFlows, - }; - }, - }); - } + listServiceChains as listServiceChainsBusiness, + getServiceChainDetails as getServiceChainDetailsBusiness, + retryServiceChain as retryServiceChainBusiness, + validateServiceChain as validateServiceChainBusiness, + getServiceChainFlows as getServiceChainFlowsBusiness, +} from './business' +import '@old-server/types/index' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { Forbidden403 } from '@old-server/utils/errors' + +export function serviceChainRouter() { + return serverInstance.router(serviceChainContract, { + listServiceChains: async ({ request: req }) => { + const { adminPermissions } = await authUser(req) + + let body: AsyncReturnType = [] + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listServiceChainsBusiness() + } + + return { + status: 200, + body, + } + }, + + getServiceChainDetails: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403() + + const serviceChainId = params.serviceChainId + const serviceChainDetails + = await getServiceChainDetailsBusiness(serviceChainId) + + return { + status: 200, + body: serviceChainDetails, + } + }, + + retryServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403() + + const serviceChainId = params.serviceChainId + await retryServiceChainBusiness(serviceChainId) + + return { + status: 204, + body: null, + } + }, + + validateServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403() + + const serviceChainId = params.validationId + await validateServiceChainBusiness(serviceChainId) + + return { + status: 204, + body: null, + } + }, + + getServiceChainFlows: async ({ params, request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403() + + const serviceChainId = params.serviceChainId + const serviceChainFlows + = await getServiceChainFlowsBusiness(serviceChainId) + + return { + status: 200, + body: serviceChainFlows, + } + }, + + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts index 829e2e172..fa61d5a6d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts @@ -1,9 +1,9 @@ -import { services } from '@cpn-console/hooks'; +import { services } from '@cpn-console/hooks' export function checkServicesHealth() { - return services.getStatus(); + return services.getStatus() } export async function refreshServicesHealth() { - return Promise.all(services.refreshStatus()); + return Promise.all(services.refreshStatus()) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts index baf5ce6aa..1960f93bf 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts @@ -1,104 +1,78 @@ -import type { ServiceStatus } from '@cpn-console/hooks'; -import { MonitorStatus, serviceContract } from '@cpn-console/shared'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest' +import { MonitorStatus, serviceContract } from '@cpn-console/shared' +import type { ServiceStatus } from '@cpn-console/hooks' +import app from '../../app' +import * as business from './business' +import { getUserMockInfos } from '../../utils/mocks' +import * as utilsController from '../../utils/controller' -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; +const authUserMock = vi.spyOn(utilsController, 'authUser') -const authUserMock = vi.spyOn(utilsController, 'authUser'); - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const businessCheckMock = vi.spyOn(business, 'checkServicesHealth'); -const businessRefreshMock = vi.spyOn(business, 'refreshServicesHealth'); +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const businessCheckMock = vi.spyOn(business, 'checkServicesHealth') +const businessRefreshMock = vi.spyOn(business, 'refreshServicesHealth') describe('test serviceContract', () => { - const services: ServiceStatus[] = [ - { - interval: 1, - lastUpdateTimestamp: 1, - message: 'OK', - name: 'A service', - status: MonitorStatus.OK, - }, - ]; - const servicesComplete: ServiceStatus[] = [ - { - cause: 'error', - interval: 1, - lastUpdateTimestamp: 1, - message: 'OK', - name: 'A service', - status: MonitorStatus.OK, - }, - ]; - - it('should return complete services, with cause', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessCheckMock.mockReturnValue(servicesComplete); - const response = await app - .inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end(); - - expect(response.json()).toStrictEqual(servicesComplete); - expect(response.statusCode).toEqual(200); - }); - - it('should not return complete services, forbidden', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - businessCheckMock.mockReturnValue(servicesComplete); - const response = await app - .inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end(); - - expect(response.statusCode).toEqual(403); - }); - - it('should return services', async () => { - businessCheckMock.mockReturnValue(servicesComplete); - const response = await app - .inject() - .get(serviceContract.getServiceHealth.path) - .end(); - - expect(response.json()).toStrictEqual(services); - expect(response.statusCode).toEqual(200); - }); - - it('should refresh services', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessRefreshMock.mockResolvedValue(servicesComplete); - const response = await app - .inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end(); - - expect(response.json()).toStrictEqual(servicesComplete); - expect(response.statusCode).toEqual(200); - }); - - it('should refresh services, cause forbidden', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - businessRefreshMock.mockResolvedValue(servicesComplete); - const response = await app - .inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end(); - - expect(response.statusCode).toEqual(403); - }); -}); + const services: ServiceStatus[] = [{ interval: 1, lastUpdateTimestamp: 1, message: 'OK', name: 'A service', status: MonitorStatus.OK }] + const servicesComplete: ServiceStatus[] = [{ cause: 'error', interval: 1, lastUpdateTimestamp: 1, message: 'OK', name: 'A service', status: MonitorStatus.OK }] + + it('should return complete services, with cause', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessCheckMock.mockReturnValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end() + + expect(response.json()).toStrictEqual(servicesComplete) + expect(response.statusCode).toEqual(200) + }) + + it('should not return complete services, forbidden', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + businessCheckMock.mockReturnValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end() + + expect(response.statusCode).toEqual(403) + }) + + it('should return services', async () => { + businessCheckMock.mockReturnValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getServiceHealth.path) + .end() + + expect(response.json()).toStrictEqual(services) + expect(response.statusCode).toEqual(200) + }) + + it('should refresh services', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessRefreshMock.mockResolvedValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end() + + expect(response.json()).toStrictEqual(servicesComplete) + expect(response.statusCode).toEqual(200) + }) + + it('should refresh services, cause forbidden', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + businessRefreshMock.mockResolvedValue(servicesComplete) + const response = await app.inject() + .get(serviceContract.getCompleteServiceHealth.path) + .end() + + expect(response.statusCode).toEqual(403) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts index f7f214716..77471d060 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts @@ -1,52 +1,43 @@ -import { AdminAuthorized, serviceContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403 } from '@old-server/utils/errors'; - -import { checkServicesHealth, refreshServicesHealth } from './business'; - -@Injectable() -export class ServiceMonitorRouterService { - constructor(private readonly appService: AppService) {} - - serviceMonitorRouter() { - return this.appService.serverInstance.router(serviceContract, { - getServiceHealth: async () => { - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - - getCompleteServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - - refreshServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - - await refreshServicesHealth(); - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - }); - } +import { AdminAuthorized, serviceContract } from '@cpn-console/shared' +import { checkServicesHealth, refreshServicesHealth } from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { Forbidden403 } from '@old-server/utils/errors' + +export function serviceMonitorRouter() { + return serverInstance.router(serviceContract, { + getServiceHealth: async () => { + const serviceData = checkServicesHealth() + + return { + status: 200, + body: serviceData, + } + }, + + getCompleteServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req) + + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + const serviceData = checkServicesHealth() + + return { + status: 200, + body: serviceData, + } + }, + + refreshServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + + await refreshServicesHealth() + const serviceData = checkServicesHealth() + + return { + status: 200, + body: serviceData, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts index 235ba727c..9c4f914f9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts @@ -1,169 +1,113 @@ -import { faker } from '@faker-js/faker'; -import type { Environment, Stage } from '@prisma/client'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import { BadRequest400, NotFound404 } from '../../utils/errors'; -import { - createStage, - deleteStage, - getStageAssociatedEnvironments, - listStages, - updateStage, -} from './business'; +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Environment, Stage } from '@prisma/client' +import prisma from '../../__mocks__/prisma' +import { BadRequest400, NotFound404 } from '../../utils/errors' +import { createStage, deleteStage, getStageAssociatedEnvironments, listStages, updateStage } from './business' describe('test stage busines logic', () => { - let stage: Stage; - beforeEach(() => { - vi.resetAllMocks(); - stage = { - id: faker.string.uuid(), - name: faker.company.name(), - }; - }); - describe('createStage', () => { - it('should create a stage', async () => { - prisma.stage.findUnique.mockResolvedValue(null); - prisma.stage.create.mockResolvedValue({ id: stage.id } as Stage); - await createStage({ - name: stage.name, - clusterIds: [faker.string.uuid()], - }); - expect(prisma.stage.update).toHaveBeenCalledTimes(1); - }); - it('should not create a stage, name conflict', async () => { - prisma.stage.findUnique.mockResolvedValue({ - id: stage.id, - } as Stage); - const response = await createStage({ - name: stage.name, - clusterIds: [faker.string.uuid()], - }); - expect(prisma.stage.update).toHaveBeenCalledTimes(0); - expect(response).instanceOf(BadRequest400); - }); - }); + let stage: Stage + beforeEach(() => { + vi.resetAllMocks() + stage = { + id: faker.string.uuid(), + name: faker.company.name(), + } + }) + describe('createStage', () => { + it('should create a stage', async () => { + prisma.stage.findUnique.mockResolvedValue(null) + prisma.stage.create.mockResolvedValue({ id: stage.id } as Stage) + await createStage({ name: stage.name, clusterIds: [faker.string.uuid()] }) + expect(prisma.stage.update).toHaveBeenCalledTimes(1) + }) + it('should not create a stage, name conflict', async () => { + prisma.stage.findUnique.mockResolvedValue({ id: stage.id } as Stage) + const response = await createStage({ name: stage.name, clusterIds: [faker.string.uuid()] }) + expect(prisma.stage.update).toHaveBeenCalledTimes(0) + expect(response).instanceOf(BadRequest400) + }) + }) - describe('updateStage', () => { - it('should update a stage', async () => { - const dbClusters = [{ id: faker.string.uuid() }]; - const newClusters = [faker.string.uuid()]; - prisma.stage.findUnique.mockResolvedValue({ - ...stage, - clusters: dbClusters, - } as Stage); - prisma.stage.update.mockResolvedValue({ id: stage.id } as Stage); - const response = await updateStage(stage.id, { - name: stage.name, - clusterIds: newClusters, - }); - expect(prisma.cluster.update).toHaveBeenCalledTimes(1); - expect(prisma.cluster.update).toHaveBeenCalledWith({ - where: { id: dbClusters[0].id }, - data: { - stages: { - disconnect: { - id: stage.id, - }, - }, - }, - }); - expect(prisma.stage.update).toHaveBeenCalledTimes(1); - expect(prisma.stage.update).toHaveBeenCalledWith({ - where: { id: stage.id }, - data: { - clusters: { - connect: [ - { - id: newClusters[0], - }, - ], - }, - }, - }); - expect(response.clusterIds).toBe(newClusters); - }); - it('should do nothing', async () => { - prisma.stage.findUnique.mockResolvedValue({ - ...stage, - clusters: [], - } as Stage); - await updateStage(stage.id, { clusterIds: [], name: stage.name }); - expect(prisma.stage.update).toHaveBeenCalledTimes(0); - }); - it('should return not found', async () => { - prisma.stage.findUnique.mockResolvedValue(null); - const response = await updateStage(stage.id, { - name: stage.name, - clusterIds: [faker.string.uuid()], - }); - expect(prisma.stage.update).toHaveBeenCalledTimes(0); - expect(response).instanceOf(NotFound404); - }); - }); + describe('updateStage', () => { + it('should update a stage', async () => { + const dbClusters = [{ id: faker.string.uuid() }] + const newClusters = [faker.string.uuid()] + prisma.stage.findUnique.mockResolvedValue({ ...stage, clusters: dbClusters } as Stage) + prisma.stage.update.mockResolvedValue({ id: stage.id } as Stage) + const response = await updateStage(stage.id, { name: stage.name, clusterIds: newClusters }) + expect(prisma.cluster.update).toHaveBeenCalledTimes(1) + expect(prisma.cluster.update).toHaveBeenCalledWith({ where: { id: dbClusters[0].id }, data: { + stages: { + disconnect: { + id: stage.id, + }, + }, + } }) + expect(prisma.stage.update).toHaveBeenCalledTimes(1) + expect(prisma.stage.update).toHaveBeenCalledWith({ where: { id: stage.id }, data: { + clusters: { + connect: [{ + id: newClusters[0], + }], + }, + } }) + expect(response.clusterIds).toBe(newClusters) + }) + it('should do nothing', async () => { + prisma.stage.findUnique.mockResolvedValue({ ...stage, clusters: [] } as Stage) + await updateStage(stage.id, { clusterIds: [], name: stage.name }) + expect(prisma.stage.update).toHaveBeenCalledTimes(0) + }) + it('should return not found', async () => { + prisma.stage.findUnique.mockResolvedValue(null) + const response = await updateStage(stage.id, { name: stage.name, clusterIds: [faker.string.uuid()] }) + expect(prisma.stage.update).toHaveBeenCalledTimes(0) + expect(response).instanceOf(NotFound404) + }) + }) - describe('deleteStage', () => { - it('should delete a stage', async () => { - prisma.environment.findFirst.mockResolvedValue(null); - prisma.stage.delete.mockResolvedValue({ id: stage.id } as Stage); - await deleteStage(stage.id); - expect(prisma.stage.delete).toHaveBeenCalledTimes(1); - }); - it('should not delete a stage, environment attached', async () => { - prisma.environment.findFirst.mockResolvedValue({ - id: faker.string.uuid(), - } as Environment); - const response = await deleteStage(stage.id); - expect(prisma.stage.delete).toHaveBeenCalledTimes(0); - expect(response).instanceOf(BadRequest400); - }); - }); + describe('deleteStage', () => { + it('should delete a stage', async () => { + prisma.environment.findFirst.mockResolvedValue(null) + prisma.stage.delete.mockResolvedValue({ id: stage.id } as Stage) + await deleteStage(stage.id) + expect(prisma.stage.delete).toHaveBeenCalledTimes(1) + }) + it('should not delete a stage, environment attached', async () => { + prisma.environment.findFirst.mockResolvedValue({ id: faker.string.uuid() } as Environment) + const response = await deleteStage(stage.id) + expect(prisma.stage.delete).toHaveBeenCalledTimes(0) + expect(response).instanceOf(BadRequest400) + }) + }) - describe('listStages', () => { - const clusterAssociated = [{ id: faker.string.uuid() }]; - it('should list all stages (admin, no userId provided)', async () => { - prisma.stage.findMany.mockResolvedValue([ - { clusters: clusterAssociated }, - ] as unknown as Stage[]); - const response = await listStages(); - expect(response[0].clusterIds).toStrictEqual([ - clusterAssociated[0].id, - ]); - expect(prisma.stage.findMany).toHaveBeenCalledTimes(1); - expect(prisma.stage.findMany).toHaveBeenCalledWith({ - include: { clusters: true }, - }); - }); - }); + describe('listStages', () => { + const clusterAssociated = [{ id: faker.string.uuid() }] + it('should list all stages (admin, no userId provided)', async () => { + prisma.stage.findMany.mockResolvedValue([{ clusters: clusterAssociated }] as unknown as Stage[]) + const response = await listStages() + expect(response[0].clusterIds).toStrictEqual([clusterAssociated[0].id]) + expect(prisma.stage.findMany).toHaveBeenCalledTimes(1) + expect(prisma.stage.findMany).toHaveBeenCalledWith({ include: { clusters: true } }) + }) + }) - describe('getStageAssociatedEnvironments', () => { - it('should list all environments attached to a stage stages', async () => { - const envName = faker.string.alpha(8); - const projectSlug = faker.string.alpha(8); - const clusterLabel = faker.string.alpha(8); - const ownerEmail = faker.internet.email(); - const envs = [ - { - name: envName, - project: { - slug: projectSlug, - owner: { email: ownerEmail }, - }, - cluster: { label: clusterLabel }, - }, - ]; - prisma.environment.findMany.mockResolvedValue( - envs as unknown as Environment[], - ); - const response = await getStageAssociatedEnvironments(stage.id); - expect(response).toStrictEqual([ - { - name: envName, - project: projectSlug, - owner: ownerEmail, - cluster: clusterLabel, - }, - ]); - }); - }); -}); + describe('getStageAssociatedEnvironments', () => { + it('should list all environments attached to a stage stages', async () => { + const envName = faker.string.alpha(8) + const projectSlug = faker.string.alpha(8) + const clusterLabel = faker.string.alpha(8) + const ownerEmail = faker.internet.email() + const envs = [{ name: envName, project: { slug: projectSlug, owner: { email: ownerEmail } }, cluster: { label: clusterLabel } }] + prisma.environment.findMany.mockResolvedValue(envs as unknown as Environment[]) + const response = await getStageAssociatedEnvironments(stage.id) + expect(response).toStrictEqual([{ + name: envName, + project: projectSlug, + owner: ownerEmail, + cluster: clusterLabel, + }]) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts index ea3bbe6f2..24c63298a 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts @@ -1,115 +1,97 @@ -import type { CreateStageBody, UpdateStageBody } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; +import type { Cluster, Stage } from '@prisma/client' +import type { CreateStageBody, UpdateStageBody } from '@cpn-console/shared' import { - createStage as createStageQuery, - deleteStage as deleteStageQuery, - getAllStageIds, - getStageAssociatedEnvironmentById, - getStageById, - getStageByName, - linkClusterToStages as linkClusterToStagesQuery, - linkStageToClusters, - listStages as listStagesQuery, - removeClusterFromStage, - updateStageName, -} from '@old-server/resources/queries-index'; -import { BadRequest400, NotFound404 } from '@old-server/utils/errors'; -import type { Cluster, Stage } from '@prisma/client'; + createStage as createStageQuery, + deleteStage as deleteStageQuery, + getAllStageIds, + getStageAssociatedEnvironmentById, + getStageById, + getStageByName, + linkClusterToStages as linkClusterToStagesQuery, + linkStageToClusters, + listStages as listStagesQuery, + removeClusterFromStage, + updateStageName, +} from '@old-server/resources/queries-index' +import { BadRequest400, NotFound404 } from '@old-server/utils/errors' +import prisma from '@old-server/prisma' export async function getStageAssociatedEnvironments(stageId: Stage['id']) { - const environments = await getStageAssociatedEnvironmentById(stageId); - return environments.map((env) => ({ - project: env.project.slug, - name: env.name, - cluster: env.cluster.label, - owner: env.project.owner.email, - })); + const environments = await getStageAssociatedEnvironmentById(stageId) + return environments.map(env => ({ + project: env.project.slug, + name: env.name, + cluster: env.cluster.label, + owner: env.project.owner.email, + })) } export async function createStage({ clusterIds = [], name }: CreateStageBody) { - const isNameTaken = await getStageByName(name); - if (isNameTaken) - return new BadRequest400( - "Un type d'environnement portant ce nom existe déjà", - ); + const isNameTaken = await getStageByName(name) + if (isNameTaken) return new BadRequest400('Un type d\'environnement portant ce nom existe déjà') - const stage = await createStageQuery({ name }); + const stage = await createStageQuery({ name }) - if (clusterIds.length) { - await linkStageToClusters(stage.id, clusterIds); - } + if (clusterIds.length) { + await linkStageToClusters(stage.id, clusterIds) + } - return { - id: stage.id, - name: stage.name, - clusterIds, - }; + return { + id: stage.id, + name: stage.name, + clusterIds, + } } -export async function updateStage( - stageId: Stage['id'], - { clusterIds, name }: UpdateStageBody, -) { - const dbStage = await getStageById(stageId); - if (!dbStage) return new NotFound404(); - if (name !== dbStage.name) { - await updateStageName(stageId, name); - } - // Remove clusters - const dbClusters = dbStage.clusters; - if (dbClusters?.length) { - const clustersToRemove = dbClusters.filter( - (dbCluster) => !clusterIds.includes(dbCluster.id), - ); - for (const clusterToRemove of clustersToRemove) { - await removeClusterFromStage(clusterToRemove.id, stageId); - } - } - // Add clusters - if (clusterIds.length) { - await linkStageToClusters(stageId, clusterIds); +export async function updateStage(stageId: Stage['id'], { clusterIds, name }: UpdateStageBody) { + const dbStage = await getStageById(stageId) + if (!dbStage) return new NotFound404() + if (name !== dbStage.name) { + await updateStageName(stageId, name) + } + // Remove clusters + const dbClusters = dbStage.clusters + if (dbClusters?.length) { + const clustersToRemove = dbClusters.filter(dbCluster => !clusterIds.includes(dbCluster.id)) + for (const clusterToRemove of clustersToRemove) { + await removeClusterFromStage(clusterToRemove.id, stageId) } + } + // Add clusters + if (clusterIds.length) { + await linkStageToClusters(stageId, clusterIds) + } - return { - id: stageId, - name: name ?? dbStage.name, - clusterIds: clusterIds ?? dbStage.clusters.map(({ id }) => id), - }; + return { + id: stageId, + name: name ?? dbStage.name, + clusterIds: clusterIds ?? dbStage.clusters.map(({ id }) => id), + } } export async function deleteStage(stageId: Stage['id']) { - const attachedEnvironment = await prisma.environment.findFirst({ - where: { stageId }, - select: { id: true }, - }); - if (attachedEnvironment) - return new BadRequest400( - 'Impossible de supprimer le stage, des environnements en activité y ont souscrit', - ); + const attachedEnvironment = await prisma.environment.findFirst({ where: { stageId }, select: { id: true } }) + if (attachedEnvironment) return new BadRequest400('Impossible de supprimer le stage, des environnements en activité y ont souscrit') - await deleteStageQuery(stageId); - return null; + await deleteStageQuery(stageId) + return null } export async function listStages() { - const stages = await listStagesQuery(); + const stages = await listStagesQuery() - return stages.map((stage) => { - return { - id: stage.id, - name: stage.name, - clusterIds: stage.clusters.map(({ id }) => id), - }; - }); + return stages.map((stage) => { + return { + id: stage.id, + name: stage.name, + clusterIds: stage.clusters.map(({ id }) => id), + } + }) } -export async function linkClusterToStages( - clusterId: Cluster['id'], - stageIds: Stage['id'][], - linkToAll: boolean = false, -) { - if (linkToAll === true) { - stageIds = await getAllStageIds(); - } - await linkClusterToStagesQuery(clusterId, stageIds); +export async function linkClusterToStages(clusterId: Cluster['id'], stageIds: Stage['id'][], linkToAll: boolean = false) { + if (linkToAll === true) { + stageIds = await getAllStageIds() + } + await linkClusterToStagesQuery(clusterId, stageIds) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts index fb2af3b53..1015099a2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts @@ -1,116 +1,111 @@ -import prisma from '@old-server/prisma'; -import type { Cluster, Stage } from '@prisma/client'; +import type { Cluster, Stage } from '@prisma/client' +import prisma from '@old-server/prisma' export function listStages() { - return prisma.stage.findMany({ - include: { - clusters: true, - }, - }); + return prisma.stage.findMany({ + include: { + clusters: true, + }, + }) } export async function getAllStageIds() { - return ( - await prisma.stage.findMany({ - select: { - id: true, - }, - }) - ).map(({ id }) => id); + return (await prisma.stage.findMany({ + select: { + id: true, + }, + })).map(({ id }) => id) } export function getStageById(id: Stage['id']) { - return prisma.stage.findUnique({ - where: { id }, - include: { - clusters: true, - }, - }); + return prisma.stage.findUnique({ + where: { id }, + include: { + clusters: true, + }, + }) } export function getStageByIdOrThrow(id: Stage['id']) { - return prisma.stage.findUniqueOrThrow({ - where: { id }, - include: { - clusters: true, - }, - }); + return prisma.stage.findUniqueOrThrow({ + where: { id }, + include: { + clusters: true, + }, + }) } export function getStageAssociatedEnvironmentById(id: Stage['id']) { - return prisma.environment.findMany({ - where: { - stageId: id, + return prisma.environment.findMany({ + where: { + stageId: id, + }, + select: { + name: true, + cluster: { + select: { + label: true, }, + }, + project: { select: { - name: true, - cluster: { - select: { - label: true, - }, - }, - project: { - select: { - name: true, - owner: true, - slug: true, - }, - }, + name: true, + owner: true, + slug: true, }, - }); + }, + }, + }) } export function getStageAssociatedEnvironmentLengthById(id: Stage['id']) { - return prisma.environment.count({ - where: { - stageId: id, - }, - }); + return prisma.environment.count({ + where: { + stageId: id, + }, + }) } export function getStageByName(name: Stage['name']) { - return prisma.stage.findUnique({ - where: { name }, - }); + return prisma.stage.findUnique({ + where: { name }, + }) } -export function linkStageToClusters( - id: Stage['id'], - clusterIds: Cluster['id'][], -) { - return prisma.stage.update({ - where: { - id, - }, - data: { - clusters: { - connect: clusterIds.map((clusterId) => ({ id: clusterId })), - }, - }, - }); +export function linkStageToClusters(id: Stage['id'], clusterIds: Cluster['id'][]) { + return prisma.stage.update({ + where: { + id, + }, + data: { + clusters: { + connect: clusterIds.map(clusterId => ({ id: clusterId })), + }, + }, + }) } export function createStage({ name }: { name: Stage['name'] }) { - return prisma.stage.create({ - data: { - name, - }, - }); + return prisma.stage.create({ + data: { + name, + }, + }) } export function updateStageName(id: Stage['id'], name: Stage['name']) { - return prisma.stage.update({ - where: { - id, - }, - data: { - name, - }, - }); + return prisma.stage.update({ + where: { + id, + }, + data: { + name, + }, + }) } export function deleteStage(id: Stage['id']) { - return prisma.stage.delete({ - where: { id }, - }); + return prisma.stage.delete({ + where: { id }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts index f2531dc57..34fb0088e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts @@ -1,273 +1,202 @@ -import type { Stage } from '@cpn-console/shared'; -import { stageContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessListMock = vi.spyOn(business, 'listStages'); -const businessGetEnvironmentsMock = vi.spyOn( - business, - 'getStageAssociatedEnvironments', -); -const businessCreateMock = vi.spyOn(business, 'createStage'); -const businessUpdateMock = vi.spyOn(business, 'updateStage'); -const businessDeleteMock = vi.spyOn(business, 'deleteStage'); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Stage } from '@cpn-console/shared' +import { stageContract } from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getUserMockInfos } from '../../utils/mocks' +import { BadRequest400 } from '../../utils/errors' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMock = vi.spyOn(business, 'listStages') +const businessGetEnvironmentsMock = vi.spyOn(business, 'getStageAssociatedEnvironments') +const businessCreateMock = vi.spyOn(business, 'createStage') +const businessUpdateMock = vi.spyOn(business, 'updateStage') +const businessDeleteMock = vi.spyOn(business, 'deleteStage') describe('test stageContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('listStages', () => { - it('should return list of stages', async () => { - const stages = []; - businessListMock.mockResolvedValueOnce(stages); - - const response = await app - .inject() - .get(stageContract.listStages.path) - .end(); - - expect(businessListMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(stages); - expect(response.statusCode).toEqual(200); - }); - }); - - describe('getStageEnvironments', () => { - it('should return stage environments for admin', async () => { - const environments = []; - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessGetEnvironmentsMock.mockResolvedValueOnce(environments); - const response = await app - .inject() - .get( - stageContract.getStageEnvironments.path.replace( - ':stageId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(environments); - expect(response.statusCode).toEqual(200); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessGetEnvironmentsMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .get( - stageContract.getStageEnvironments.path.replace( - ':stageId', - faker.string.uuid(), - ), - ) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get( - stageContract.getStageEnvironments.path.replace( - ':stageId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('createStage', () => { - const stage: Stage = { - id: faker.string.uuid(), - name: faker.string.alpha({ length: 5 }), - clusterIds: [], - }; - - it('should create and return stage for admin', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce(stage); - const response = await app - .inject() - .post(stageContract.createStage.path) - .body(stage) - .end(); - - expect(businessCreateMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(stage); - expect(response.statusCode).toEqual(201); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .post(stageContract.createStage.path) - .body(stage) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(stageContract.createStage.path) - .body(stage) - .end(); - - expect(businessCreateMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('updateStage', () => { - const stageId = faker.string.uuid(); - const stage = { - name: faker.string.alpha({ length: 5 }), - clusterIds: [], - }; - - it('should update and return stage for admin', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce({ id: stageId, ...stage }); - const response = await app - .inject() - .put( - stageContract.updateStage.path.replace(':stageId', stageId), - ) - .body(stage) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual({ id: stageId, ...stage }); - expect(response.statusCode).toEqual(200); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .put( - stageContract.updateStage.path.replace(':stageId', stageId), - ) - .body(stage) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put( - stageContract.updateStage.path.replace(':stageId', stageId), - ) - .body(stage) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('deleteStage', () => { - it('should delete stage for admin', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce(null); - const response = await app - .inject() - .delete( - stageContract.deleteStage.path.replace( - ':stageId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessDeleteMock).toHaveBeenCalledTimes(1); - expect(response.body).toBeFalsy(); - expect(response.statusCode).toEqual(204); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .delete( - stageContract.deleteStage.path.replace( - ':stageId', - faker.string.uuid(), - ), - ) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - stageContract.deleteStage.path.replace( - ':stageId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessDeleteMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listStages', () => { + it('should return list of stages', async () => { + const stages = [] + businessListMock.mockResolvedValueOnce(stages) + + const response = await app.inject() + .get(stageContract.listStages.path) + .end() + + expect(businessListMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(stages) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('getStageEnvironments', () => { + it('should return stage environments for admin', async () => { + const environments = [] + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetEnvironmentsMock.mockResolvedValueOnce(environments) + const response = await app.inject() + .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(environments) + expect(response.statusCode).toEqual(200) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessGetEnvironmentsMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('createStage', () => { + const stage: Stage = { id: faker.string.uuid(), name: faker.string.alpha({ length: 5 }), clusterIds: [] } + + it('should create and return stage for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(stage) + const response = await app.inject() + .post(stageContract.createStage.path) + .body(stage) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(stage) + expect(response.statusCode).toEqual(201) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(stageContract.createStage.path) + .body(stage) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(stageContract.createStage.path) + .body(stage) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('updateStage', () => { + const stageId = faker.string.uuid() + const stage = { name: faker.string.alpha({ length: 5 }), clusterIds: [] } + + it('should update and return stage for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: stageId, ...stage }) + const response = await app.inject() + .put(stageContract.updateStage.path.replace(':stageId', stageId)) + .body(stage) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ id: stageId, ...stage }) + expect(response.statusCode).toEqual(200) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(stageContract.updateStage.path.replace(':stageId', stageId)) + .body(stage) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(stageContract.updateStage.path.replace(':stageId', stageId)) + .body(stage) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('deleteStage', () => { + it('should delete stage for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(1) + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts index d49a04a52..f71f0ee93 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts @@ -1,97 +1,88 @@ -import { AdminAuthorized, stageContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - +import { AdminAuthorized, stageContract } from '@cpn-console/shared' import { - createStage, - deleteStage, - getStageAssociatedEnvironments, - listStages, - updateStage, -} from './business'; - -@Injectable() -export class StageRouterService { - constructor(private readonly appService: AppService) {} - - stageRouter() { - return this.appService.serverInstance.router(stageContract, { - // Récupérer les types d'environnement disponibles - listStages: async () => { - const body = await listStages(); - - return { - status: 200, - body, - }; - }, - - // Récupérer les environnements associés au stage - getStageEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - const body = await getStageAssociatedEnvironments(stageId); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Créer un stage - createStage: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const body = await createStage(data); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Modifier une association stage / clusters - updateStage: async ({ request: req, params, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - - const body = await updateStage(stageId, data); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Supprimer un stage - deleteStage: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - - const body = await deleteStage(stageId); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); - } + createStage, + deleteStage, + getStageAssociatedEnvironments, + listStages, + updateStage, +} from './business' +import { serverInstance } from '@old-server/app' + +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' + +export function stageRouter() { + return serverInstance.router(stageContract, { + + // Récupérer les types d'environnement disponibles + listStages: async () => { + const body = await listStages() + + return { + status: 200, + body, + } + }, + + // Récupérer les environnements associés au stage + getStageEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const stageId = params.stageId + const body = await getStageAssociatedEnvironments(stageId) + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + // Créer un stage + createStage: async ({ request: req, body: data }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const body = await createStage(data) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + // Modifier une association stage / clusters + updateStage: async ({ request: req, params, body: data }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const stageId = params.stageId + + const body = await updateStage(stageId, data) + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + // Supprimer un stage + deleteStage: async ({ request: req, params }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const stageId = params.stageId + + const body = await deleteStage(stageId) + if (body instanceof ErrorResType) return body + + return { + status: 204, + body, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts index 80776dc3a..ef2b0ea71 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts @@ -1,25 +1,22 @@ -import { describe, expect, it } from 'vitest'; - -import prisma from '../../../__mocks__/prisma'; -import { objToDb, updatePluginConfig } from './business'; +import { describe, expect, it } from 'vitest' +import prisma from '../../../__mocks__/prisma' +import { objToDb, updatePluginConfig } from './business' describe('test system/config business', () => { - const config = { test: { key1: 'value1' } }; - it('should transform object to db row', () => { - const response = objToDb({ test: { key1: 'value1' } }); - expect(response).toEqual([ - { pluginName: 'test', key: 'key1', value: 'value1' }, - ]); - }); - describe('updatePluginConfig', () => { - it('should update', async () => { - prisma.adminPlugin.upsert.mockResolvedValue(null); - await updatePluginConfig(config); - }); - it('should update 0 items cause missing manifest', async () => { - // @ts-ignore - await updatePluginConfig({ test: { key: 1 } }); - expect(prisma.adminPlugin.upsert).toHaveBeenCalledTimes(0); - }); - }); -}); + const config = { test: { key1: 'value1' } } + it('should transform object to db row', () => { + const response = objToDb({ test: { key1: 'value1' } }) + expect(response).toEqual([{ pluginName: 'test', key: 'key1', value: 'value1' }]) + }) + describe('updatePluginConfig', () => { + it('should update', async () => { + prisma.adminPlugin.upsert.mockResolvedValue(null) + await updatePluginConfig(config) + }) + it('should update 0 items cause missing manifest', async () => { + // @ts-ignore + await updatePluginConfig({ test: { key: 1 } }) + expect(prisma.adminPlugin.upsert).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts index 80c85851c..a891203a7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts @@ -1,63 +1,50 @@ +import type { + PluginsUpdateBody, +} from '@cpn-console/shared' +import { editStrippers, populatePluginManifests, servicesInfos } from '@cpn-console/hooks' import { - editStrippers, - populatePluginManifests, - servicesInfos, -} from '@cpn-console/hooks'; -import type { PluginsUpdateBody } from '@cpn-console/shared'; -import { BadRequest400 } from '@old-server/utils/errors'; - -import { getAdminPlugin, savePluginsConfig } from './queries'; + getAdminPlugin, + savePluginsConfig, +} from './queries' +import { BadRequest400 } from '@old-server/utils/errors' export type ConfigRecords = { - key: string; - pluginName: string; - value: string; -}[]; + key: string + pluginName: string + value: string +}[] export function objToDb(obj: PluginsUpdateBody): ConfigRecords { - return Object.entries(obj) - .map(([pluginName, values]) => - Object.entries(values).map(([key, value]) => ({ - pluginName, - key, - value, - })), - ) - .flat(); + return Object.entries(obj) + .map(([pluginName, values]) => Object.entries(values) + .map(([key, value]) => ({ pluginName, key, value }))) + .flat() } export async function getPluginsConfig() { - const globalConfig = await getAdminPlugin(); + const globalConfig = await getAdminPlugin() - return Object.values(servicesInfos) - .map(({ name, title, imgSrc, description }) => { - const manifest = populatePluginManifests({ - data: { - global: globalConfig, - }, - permissionTarget: 'admin', - pluginName: name, - select: { - global: true, - project: false, - }, - }); - return { - imgSrc, - title, - name, - manifest: manifest.global ?? [], - description, - }; - }) - .filter((plugin) => plugin.manifest.length > 0); + return Object.values(servicesInfos).map(({ name, title, imgSrc, description }) => { + const manifest = populatePluginManifests({ + data: { + global: globalConfig, + }, + permissionTarget: 'admin', + pluginName: name, + select: { + global: true, + project: false, + }, + }) + return { imgSrc, title, name, manifest: manifest.global ?? [], description } + }).filter(plugin => plugin.manifest.length > 0) } export async function updatePluginConfig(data: PluginsUpdateBody) { - const parsedData = editStrippers.global.safeParse(data); - if (!parsedData.success) return new BadRequest400(parsedData.error.message); - const records = objToDb(parsedData.data); + const parsedData = editStrippers.global.safeParse(data) + if (!parsedData.success) return new BadRequest400(parsedData.error.message) + const records = objToDb(parsedData.data) - await savePluginsConfig(records); - return null; + await savePluginsConfig(records) + return null } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts index c9e85b634..c038c0d80 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts @@ -1,29 +1,28 @@ -import prisma from '@old-server/prisma'; - -import type { ConfigRecords } from './business'; +import type { ConfigRecords } from './business' +import prisma from '@old-server/prisma' // CONFIG -export const getAdminPlugin = prisma.adminPlugin.findMany; +export const getAdminPlugin = prisma.adminPlugin.findMany export async function savePluginsConfig(records: ConfigRecords) { - for (const { pluginName, key, value } of records) { - await prisma.adminPlugin.upsert({ - create: { - pluginName, - key, - value: String(value), - }, - update: { - key, - value: String(value), - pluginName, - }, - where: { - pluginName_key: { - pluginName, - key, - }, - }, - }); - } + for (const { pluginName, key, value } of records) { + await prisma.adminPlugin.upsert({ + create: { + pluginName, + key, + value: String(value), + }, + update: { + key, + value: String(value), + pluginName, + }, + where: { + pluginName_key: { + pluginName, + key, + }, + }, + }) + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts index 463a0b3f8..472a102a6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts @@ -1,111 +1,96 @@ -import { systemPluginContract } from '@cpn-console/shared'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../../app'; -import * as utilsController from '../../../utils/controller'; -import { BadRequest400 } from '../../../utils/errors'; -import { getUserMockInfos } from '../../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessGetPluginsConfigMock = vi.spyOn(business, 'getPluginsConfig'); -const businessUpdatePluginConfigMock = vi.spyOn(business, 'updatePluginConfig'); +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { systemPluginContract } from '@cpn-console/shared' +import app from '../../../app' +import * as utilsController from '../../../utils/controller' +import { getUserMockInfos } from '../../../utils/mocks' +import { BadRequest400 } from '../../../utils/errors' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetPluginsConfigMock = vi.spyOn(business, 'getPluginsConfig') +const businessUpdatePluginConfigMock = vi.spyOn(business, 'updatePluginConfig') describe('test systemPluginContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('getPluginsConfig', () => { - it('should return plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true); - const pluginsConfig = []; - - authUserMock.mockResolvedValueOnce(user); - businessGetPluginsConfigMock.mockResolvedValueOnce(pluginsConfig); - - const response = await app - .inject() - .get(systemPluginContract.getPluginsConfig.path) - .end(); - - expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(pluginsConfig); - expect(response.statusCode).toEqual(200); - }); - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(systemPluginContract.getPluginsConfig.path) - .end(); - - expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('updatePluginsConfig', () => { - const newConfig = { plugin1: { keyId: 'value' } }; - it('should update plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessUpdatePluginConfigMock.mockResolvedValueOnce(newConfig); - - const response = await app - .inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end(); - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith( - newConfig, - ); - expect(response.statusCode).toEqual(204); - }); - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false); - - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end(); - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - - it('should return error if business logic fails', async () => { - const user = getUserMockInfos(true); - - authUserMock.mockResolvedValueOnce(user); - businessUpdatePluginConfigMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - - const response = await app - .inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end(); - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith( - newConfig, - ); - expect(response.statusCode).toEqual(400); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('getPluginsConfig', () => { + it('should return plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true) + const pluginsConfig = [] + + authUserMock.mockResolvedValueOnce(user) + businessGetPluginsConfigMock.mockResolvedValueOnce(pluginsConfig) + + const response = await app.inject() + .get(systemPluginContract.getPluginsConfig.path) + .end() + + expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(pluginsConfig) + expect(response.statusCode).toEqual(200) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(systemPluginContract.getPluginsConfig.path) + .end() + + expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('updatePluginsConfig', () => { + const newConfig = { plugin1: { keyId: 'value' } } + it('should update plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessUpdatePluginConfigMock.mockResolvedValueOnce(newConfig) + + const response = await app.inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end() + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith(newConfig) + expect(response.statusCode).toEqual(204) + }) + + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) + + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end() + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + + it('should return error if business logic fails', async () => { + const user = getUserMockInfos(true) + + authUserMock.mockResolvedValueOnce(user) + businessUpdatePluginConfigMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + + const response = await app.inject() + .post(systemPluginContract.updatePluginsConfig.path) + .body(newConfig) + .end() + + expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith(newConfig) + expect(response.statusCode).toEqual(400) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts index 6b5b8ec57..df4179a47 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts @@ -1,44 +1,36 @@ -import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; +import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared' +import { getPluginsConfig, updatePluginConfig } from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' -import { getPluginsConfig, updatePluginConfig } from './business'; +export function pluginConfigRouter() { + return serverInstance.router(systemPluginContract, { + // Récupérer les configurations plugins + getPluginsConfig: async ({ request: req }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() -@Injectable() -export class SystemConfigRouterService { - constructor(private readonly appService: AppService) {} + const services = await getPluginsConfig() - pluginConfigRouter() { - return this.appService.serverInstance.router(systemPluginContract, { - // Récupérer les configurations plugins - getPluginsConfig: async ({ request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); + return { + status: 200, + body: services, - const services = await getPluginsConfig(); + } + }, + // Mettre à jour les configurations plugins + updatePluginsConfig: async ({ request: req, body }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - return { - status: 200, - body: services, - }; - }, - // Mettre à jour les configurations plugins - updatePluginsConfig: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); + const resBody = await updatePluginConfig(body) + if (resBody instanceof ErrorResType) return resBody - const resBody = await updatePluginConfig(body); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 204, - body: resBody, - }; - }, - }); - } + return { + status: 204, + body: resBody, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts index 164ab508b..398cae734 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts @@ -1 +1 @@ -export * from './router'; +export * from './router' diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts index 0c58a3ae8..9ff040c6c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts @@ -1,33 +1,25 @@ -import { systemContract } from '@cpn-console/shared'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest' +import { systemContract } from '@cpn-console/shared' +import app from '../../app' -import app from '../../app'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) describe('system - router', () => { - it('should send application version', async () => { - const response = await app - .inject() - .get(systemContract.getVersion.path) - .end(); + it('should send application version', async () => { + const response = await app.inject() + .get(systemContract.getVersion.path) + .end() - expect(response.statusCode).toBe(200); - expect(response.json()).toStrictEqual({ - version: process.env.APP_VERSION || 'dev', - }); - }); + expect(response.statusCode).toBe(200) + expect(response.json()).toStrictEqual({ version: process.env.APP_VERSION || 'dev' }) + }) - it('should send application health with status OK', async () => { - const response = await app - .inject() - .get(systemContract.getHealth.path) - .end(); + it('should send application health with status OK', async () => { + const response = await app.inject() + .get(systemContract.getHealth.path) + .end() - expect(response.statusCode).toBe(200); - expect(response.json()).toStrictEqual({ status: 'OK' }); - }); -}); + expect(response.statusCode).toBe(200) + expect(response.json()).toStrictEqual({ status: 'OK' }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts index 17a2c4457..730dfe083 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts @@ -1,27 +1,21 @@ -import { systemContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { appVersion } from '@old-server/utils/env'; +import { systemContract } from '@cpn-console/shared' +import { serverInstance } from '@old-server/app' +import { appVersion } from '@old-server/utils/env' -@Injectable() -export class SystemRouterService { - constructor(private readonly appService: AppService) {} +export function systemRouter() { + return serverInstance.router(systemContract, { + getVersion: async () => ({ + status: 200, + body: { + version: appVersion, + }, + }), - systemRouter() { - return this.appService.serverInstance.router(systemContract, { - getVersion: async () => ({ - status: 200, - body: { - version: appVersion, - }, - }), - - getHealth: async () => ({ - status: 200, - body: { - status: 'OK', - }, - }), - }); - } + getHealth: async () => ({ + status: 200, + body: { + status: 'OK', + }, + }), + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts index 80a8b0b8d..da540b473 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts @@ -1,13 +1,9 @@ -import type { UpsertSystemSettingBody } from '@cpn-console/shared'; - +import type { UpsertSystemSettingBody } from '@cpn-console/shared' import { - getSystemSettings as getSystemSettingsQuery, - upsertSystemSetting as upsertSystemSettingQuery, -} from './queries'; + getSystemSettings as getSystemSettingsQuery, + upsertSystemSetting as upsertSystemSettingQuery, +} from './queries' -export const getSystemSettings = (key?: string) => - getSystemSettingsQuery({ key }); +export const getSystemSettings = (key?: string) => getSystemSettingsQuery({ key }) -export const upsertSystemSetting = ( - newSystemSetting: UpsertSystemSettingBody, -) => upsertSystemSettingQuery(newSystemSetting); +export const upsertSystemSetting = (newSystemSetting: UpsertSystemSettingBody) => upsertSystemSettingQuery(newSystemSetting) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts index 252a58400..93a8ba02f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts @@ -1,19 +1,18 @@ -import prisma from '@old-server/prisma'; -import type { Prisma, SystemSetting } from '@prisma/client'; +import type { Prisma, SystemSetting } from '@prisma/client' +import prisma from '@old-server/prisma' export function upsertSystemSetting(newSystemSetting: SystemSetting) { - return prisma.systemSetting.upsert({ - create: { - ...newSystemSetting, - }, - update: { - value: newSystemSetting.value, - }, - where: { - key: newSystemSetting.key, - }, - }); + return prisma.systemSetting.upsert({ + create: { + ...newSystemSetting, + }, + update: { + value: newSystemSetting.value, + }, + where: { + key: newSystemSetting.key, + }, + }) } -export const getSystemSettings = (where?: Prisma.SystemSettingWhereInput) => - prisma.systemSetting.findMany({ where }); +export const getSystemSettings = (where?: Prisma.SystemSettingWhereInput) => prisma.systemSetting.findMany({ where }) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts index 6f135b3ae..b48c3d6ea 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts @@ -1,79 +1,67 @@ -import { systemSettingsContract } from '@cpn-console/shared'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { systemSettingsContract } from '@cpn-console/shared' +import app from '../../../app' +import * as utilsController from '../../../utils/controller' +import { getUserMockInfos } from '../../../utils/mocks' +import * as business from './business' -import app from '../../../app'; -import * as utilsController from '../../../utils/controller'; -import { getUserMockInfos } from '../../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessGetSystemSettingsMock = vi.spyOn(business, 'getSystemSettings'); -const businessUpsertSystemSettingMock = vi.spyOn( - business, - 'upsertSystemSetting', -); +vi.mock('fastify-keycloak-adapter', (await import('../../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetSystemSettingsMock = vi.spyOn(business, 'getSystemSettings') +const businessUpsertSystemSettingMock = vi.spyOn(business, 'upsertSystemSetting') describe('test systemSettingsContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); + beforeEach(() => { + vi.resetAllMocks() + }) - describe('listSystemSettings', () => { - it('should return plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true); - const systemSettings = []; + describe('listSystemSettings', () => { + it('should return plugin configurations for authorized users', async () => { + const user = getUserMockInfos(true) + const systemSettings = [] - authUserMock.mockResolvedValueOnce(user); - businessGetSystemSettingsMock.mockResolvedValueOnce(systemSettings); + authUserMock.mockResolvedValueOnce(user) + businessGetSystemSettingsMock.mockResolvedValueOnce(systemSettings) - const response = await app - .inject() - .get(systemSettingsContract.listSystemSettings.path) - .end(); + const response = await app.inject() + .get(systemSettingsContract.listSystemSettings.path) + .end() - expect(businessGetSystemSettingsMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(systemSettings); - expect(response.statusCode).toEqual(200); - }); - }); + expect(businessGetSystemSettingsMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(systemSettings) + expect(response.statusCode).toEqual(200) + }) + }) - describe('upsertSystemSetting', () => { - const newConfig = { key: 'key1', value: 'value1' }; - it('should update system setting, authorized users', async () => { - const user = getUserMockInfos(true); + describe('upsertSystemSetting', () => { + const newConfig = { key: 'key1', value: 'value1' } + it('should update system setting, authorized users', async () => { + const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user); - businessUpsertSystemSettingMock.mockResolvedValueOnce(newConfig); + authUserMock.mockResolvedValueOnce(user) + businessUpsertSystemSettingMock.mockResolvedValueOnce(newConfig) - const response = await app - .inject() - .post(systemSettingsContract.upsertSystemSetting.path) - .body(newConfig) - .end(); + const response = await app.inject() + .post(systemSettingsContract.upsertSystemSetting.path) + .body(newConfig) + .end() - expect(businessUpsertSystemSettingMock).toHaveBeenCalledWith( - newConfig, - ); - expect(response.statusCode).toEqual(201); - }); + expect(businessUpsertSystemSettingMock).toHaveBeenCalledWith(newConfig) + expect(response.statusCode).toEqual(201) + }) - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false); + it('should return 403 for unauthorized users', async () => { + const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user); + authUserMock.mockResolvedValueOnce(user) - const response = await app - .inject() - .post(systemSettingsContract.upsertSystemSetting.path) - .body(newConfig) - .end(); + const response = await app.inject() + .post(systemSettingsContract.upsertSystemSetting.path) + .body(newConfig) + .end() - expect(businessUpsertSystemSettingMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); -}); + expect(businessUpsertSystemSettingMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts index 931e23fc6..e1ed3a6a8 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts @@ -1,38 +1,30 @@ -import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403 } from '@old-server/utils/errors'; +import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared' +import { getSystemSettings, upsertSystemSetting } from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { Forbidden403 } from '@old-server/utils/errors' -import { getSystemSettings, upsertSystemSetting } from './business'; +export function systemSettingsRouter() { + return serverInstance.router(systemSettingsContract, { + listSystemSettings: async ({ query }) => { + const systemSettings = await getSystemSettings(query.key) -@Injectable() -export class SystemSettingsRouterService { - constructor(private readonly appService: AppService) {} + return { + status: 200, + body: systemSettings, + } + }, - systemSettingsRouter() { - return this.appService.serverInstance.router(systemSettingsContract, { - listSystemSettings: async ({ query }) => { - const systemSettings = await getSystemSettings(query.key); + upsertSystemSetting: async ({ request: req, body: data }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - return { - status: 200, - body: systemSettings, - }; - }, + const systemSetting = await upsertSystemSetting(data) - upsertSystemSetting: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const systemSetting = await upsertSystemSetting(data); - - return { - status: 201, - body: systemSetting, - }; - }, - }); - } + return { + status: 201, + body: systemSetting, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts index ffb590492..2b244fd0f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts @@ -1,255 +1,222 @@ -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import prisma from '../../__mocks__/prisma'; -import type { UserDetails } from '../../types/index'; -import { - TokenInvalidReason, - getMatchingUsers, - getUsers, - logViaSession, - logViaToken, - patchUsers, -} from './business'; -import * as queries from './queries'; - -const getUsersQueryMock = vi.spyOn(queries, 'getUsers'); -const getMatchingUsersQueryMock = vi.spyOn(queries, 'getMatchingUsers'); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import prisma from '../../__mocks__/prisma' +import type { UserDetails } from '../../types/index' +import { TokenInvalidReason, getMatchingUsers, getUsers, logViaSession, logViaToken, patchUsers } from './business' +import * as queries from './queries' + +const getUsersQueryMock = vi.spyOn(queries, 'getUsers') +const getMatchingUsersQueryMock = vi.spyOn(queries, 'getMatchingUsers') describe('test users business', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - const user = { - adminRoleIds: [], - createdAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - id: faker.string.uuid(), - lastName: faker.person.lastName(), - updatedAt: new Date(), - }; - const projectId = faker.string.uuid(); - const adminRoleId = faker.string.uuid(); - describe('patchUsers', () => { - it('should do nothing', async () => { - prisma.user.update.mockResolvedValue(null); - - await patchUsers([]); - - expect(prisma.user.update).toHaveBeenCalledTimes(0); - }); - - it('should update a user adminRoleIds', async () => { - const userUpdated = { - id: user.id, - adminRoleIds: user.adminRoleIds, - }; - - prisma.user.update.mockResolvedValue(user); - - prisma.user.findMany.mockResolvedValue([]); - - await patchUsers([userUpdated]); - expect(prisma.user.update).toHaveBeenCalledTimes(1); - expect(prisma.user.findMany).toHaveBeenCalledTimes(1); - - await patchUsers([userUpdated, userUpdated]); - expect(prisma.user.update).toHaveBeenCalledTimes(3); - }); - }); - describe('getUsers', () => { - it('should query without where', async () => { - prisma.user.update.mockResolvedValue(null); - - await getUsers({}); - - expect(getUsersQueryMock).toHaveBeenCalledTimes(1); - expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [] }); - }); - it('should query with filter adminRoleIds', async () => { - prisma.user.update.mockResolvedValue(null); - - await getUsers({ adminRoleIds: [adminRoleId] }); - - expect(getUsersQueryMock).toHaveBeenCalledTimes(1); - expect(getUsersQueryMock).toHaveBeenCalledWith({ - AND: [{ adminRoleIds: { hasEvery: [adminRoleId] } }], - }); - }); - }); - - describe('getMatchingUsers', () => { - const AND = [ - { - OR: [ - { - email: { - contains: 'abc', - mode: 'insensitive', - }, - }, - { - firstName: { - contains: 'abc', - mode: 'insensitive', - }, - }, - { - lastName: { - contains: 'abc', - mode: 'insensitive', - }, - }, - ], - }, - { - type: 'human', + beforeEach(() => { + vi.resetAllMocks() + }) + + const user = { + adminRoleIds: [], + createdAt: new Date(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + id: faker.string.uuid(), + lastName: faker.person.lastName(), + updatedAt: new Date(), + } + const projectId = faker.string.uuid() + const adminRoleId = faker.string.uuid() + describe('patchUsers', () => { + it('should do nothing', async () => { + prisma.user.update.mockResolvedValue(null) + + await patchUsers([]) + + expect(prisma.user.update).toHaveBeenCalledTimes(0) + }) + + it('should update a user adminRoleIds', async () => { + const userUpdated = { id: user.id, adminRoleIds: user.adminRoleIds } + + prisma.user.update.mockResolvedValue(user) + + prisma.user.findMany.mockResolvedValue([]) + + await patchUsers([userUpdated]) + expect(prisma.user.update).toHaveBeenCalledTimes(1) + expect(prisma.user.findMany).toHaveBeenCalledTimes(1) + + await patchUsers([userUpdated, userUpdated]) + expect(prisma.user.update).toHaveBeenCalledTimes(3) + }) + }) + describe('getUsers', () => { + it('should query without where', async () => { + prisma.user.update.mockResolvedValue(null) + + await getUsers({}) + + expect(getUsersQueryMock).toHaveBeenCalledTimes(1) + expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [] }) + }) + it('should query with filter adminRoleIds', async () => { + prisma.user.update.mockResolvedValue(null) + + await getUsers({ adminRoleIds: [adminRoleId] }) + + expect(getUsersQueryMock).toHaveBeenCalledTimes(1) + expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [{ adminRoleIds: { hasEvery: [adminRoleId] } }] }) + }) + }) + + describe('getMatchingUsers', () => { + const AND = [ + { + OR: [ + { + email: { + contains: 'abc', + mode: 'insensitive', }, - ]; - it('should query only with letters ', async () => { - prisma.user.update.mockResolvedValue(null); - - await getMatchingUsers({ letters: 'abc' }); - - expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1); - expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND }); - }); - it('should query with letters and projectId', async () => { - prisma.user.update.mockResolvedValue(null); - - await getMatchingUsers({ - letters: 'abc', - notInProjectId: projectId, - }); - - expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1); - expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ - AND: [ - { - projectMembers: { - none: { - projectId, - }, - }, - }, - { - projectsOwned: { - none: { - id: projectId, - }, - }, - }, - ].concat(AND), - }); - }); - }); - describe('logViaSession', () => { - // ça ne teste pas tout mais c'est déjà bien hein - const adminRoles = [ - { - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: 0n, - position: 0, + }, + { + firstName: { + contains: 'abc', + mode: 'insensitive', }, - { - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '/admin', - permissions: 0n, - position: 0, + }, + { + lastName: { + contains: 'abc', + mode: 'insensitive', }, - ]; - const userToLog: UserDetails = { - id: faker.string.uuid(), - email: user.email, - firstName: user.firstName, - groups: [], - lastName: user.lastName, - }; - it('should create user and return adminPerms', async () => { - prisma.adminRole.findMany.mockResolvedValue(adminRoles); - prisma.user.findUnique.mockResolvedValue(undefined); - prisma.user.create.mockResolvedValue(user); - prisma.user.update.mockResolvedValue(user); - const response = await logViaSession(userToLog); - expect(response.adminPerms).toBe(0n); - expect(prisma.user.create).toHaveBeenCalledTimes(1); - }); - it('should update user and return adminPerms', async () => { - prisma.adminRole.findMany.mockResolvedValue(adminRoles); - prisma.user.findUnique.mockResolvedValue(user); - prisma.user.update.mockResolvedValue(user); - const response = await logViaSession(userToLog); - expect(response.adminPerms).toEqual(0n); - expect(prisma.user.create).toHaveBeenCalledTimes(0); - }); - }); -}); + }, + ], + }, + { + type: 'human', + }, + ] + it('should query only with letters ', async () => { + prisma.user.update.mockResolvedValue(null) + + await getMatchingUsers({ letters: 'abc' }) + + expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1) + expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND }) + }) + it('should query with letters and projectId', async () => { + prisma.user.update.mockResolvedValue(null) + + await getMatchingUsers({ letters: 'abc', notInProjectId: projectId }) + + expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1) + expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND: [{ + projectMembers: { + none: { + projectId, + }, + }, + }, { + projectsOwned: { + none: { + id: projectId, + }, + }, + }].concat(AND) }) + }) + }) + describe('logViaSession', () => { + // ça ne teste pas tout mais c'est déjà bien hein + const adminRoles = [{ + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '', + permissions: 0n, + position: 0, + }, { + id: faker.string.uuid(), + name: faker.company.name(), + oidcGroup: '/admin', + permissions: 0n, + position: 0, + }] + const userToLog: UserDetails = { + id: faker.string.uuid(), + email: user.email, + firstName: user.firstName, + groups: [], + lastName: user.lastName, + } + it('should create user and return adminPerms', async () => { + prisma.adminRole.findMany.mockResolvedValue(adminRoles) + prisma.user.findUnique.mockResolvedValue(undefined) + prisma.user.create.mockResolvedValue(user) + prisma.user.update.mockResolvedValue(user) + const response = await logViaSession(userToLog) + expect(response.adminPerms).toBe(0n) + expect(prisma.user.create).toHaveBeenCalledTimes(1) + }) + it('should update user and return adminPerms', async () => { + prisma.adminRole.findMany.mockResolvedValue(adminRoles) + prisma.user.findUnique.mockResolvedValue(user) + prisma.user.update.mockResolvedValue(user) + const response = await logViaSession(userToLog) + expect(response.adminPerms).toEqual(0n) + expect(prisma.user.create).toHaveBeenCalledTimes(0) + }) + }) +}) describe('logViaToken', () => { - const nextYear = new Date(); - const lastYear = new Date(); - nextYear.setFullYear(new Date().getFullYear() + 1); - lastYear.setFullYear(new Date().getFullYear() - 1); - const baseToken = { - createdAt: new Date(), - hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', - id: faker.string.uuid(), - lastUse: null, - permissions: 2n, - userId: null, - status: 'active', - } as const; - - it('should return identity', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken }); - const identity = await logViaToken('test'); - expect(identity.adminPerms).toBe(2n); - }); - - it('should return identity based on pat', async () => { - const pat = structuredClone(baseToken); - delete pat.permissions; - pat.owner = { adminRoleIds: null }; - prisma.personalAccessToken.findFirst.mockResolvedValueOnce(pat); - const identity = await logViaToken('test'); - expect(identity.adminPerms).toBe(0n); - }); - - it('should return identity, with expirationDate', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ - ...baseToken, - expirationDate: nextYear, - }); - const identity = await logViaToken('test'); - expect(identity.adminPerms).toBe(2n); - }); - - it('should return cause revoked', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ - ...baseToken, - status: 'revoked', - }); - const identity = await logViaToken('test'); - expect(identity).toBe(TokenInvalidReason.INACTIVE); - }); - - it('should return cause expired', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ - ...baseToken, - expirationDate: lastYear, - }); - const identity = await logViaToken('test'); - expect(identity).toBe(TokenInvalidReason.EXPIRED); - }); - - it('should return cause not found', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce(undefined); - const identity = await logViaToken('test'); - expect(identity).toBe(TokenInvalidReason.NOT_FOUND); - }); -}); + const nextYear = new Date() + const lastYear = new Date() + nextYear.setFullYear((new Date()).getFullYear() + 1) + lastYear.setFullYear((new Date()).getFullYear() - 1) + const baseToken = { + createdAt: new Date(), + hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + id: faker.string.uuid(), + lastUse: null, + permissions: 2n, + userId: null, + status: 'active', + } as const + + it('should return identity', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken }) + const identity = await logViaToken('test') + expect(identity.adminPerms).toBe(2n) + }) + + it('should return identity based on pat', async () => { + const pat = structuredClone(baseToken) + delete pat.permissions + pat.owner = { adminRoleIds: null } + prisma.personalAccessToken.findFirst.mockResolvedValueOnce(pat) + const identity = await logViaToken('test') + expect(identity.adminPerms).toBe(0n) + }) + + it('should return identity, with expirationDate', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, expirationDate: nextYear }) + const identity = await logViaToken('test') + expect(identity.adminPerms).toBe(2n) + }) + + it('should return cause revoked', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, status: 'revoked' }) + const identity = await logViaToken('test') + expect(identity).toBe(TokenInvalidReason.INACTIVE) + }) + + it('should return cause expired', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, expirationDate: lastYear }) + const identity = await logViaToken('test') + expect(identity).toBe(TokenInvalidReason.EXPIRED) + }) + + it('should return cause not found', async () => { + prisma.adminToken.findFirst.mockResolvedValueOnce(undefined) + const identity = await logViaToken('test') + expect(identity).toBe(TokenInvalidReason.NOT_FOUND) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts index 495b39011..f1135e2ef 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts @@ -1,295 +1,201 @@ -import type { XOR, userContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; -import { - getMatchingUsers as getMatchingUsersQuery, - getUsers as getUsersQuery, -} from '@old-server/resources/queries-index'; -import type { UserDetails } from '@old-server/types/index'; -import { BadRequest400 } from '@old-server/utils/errors'; -import type { - AdminRole, - AdminToken, - PersonalAccessToken, - Prisma, - User, -} from '@prisma/client'; -import { createHash } from 'node:crypto'; - -export async function getUsers( - query: typeof userContract.getAllUsers.query._type, - relationType: 'OR' | 'AND' = 'AND', -) { - const whereInputs: Prisma.UserWhereInput[] = []; - if (query.adminRoleIds?.length) { - whereInputs.push({ adminRoleIds: { hasEvery: query.adminRoleIds } }); - } - if (query.adminRoles?.length) { - const roles = query.adminRoles - ? await prisma.adminRole.findMany({ - where: { name: { in: query.adminRoles } }, - }) - : []; - - const adminRoleNameNotFound = query.adminRoles?.find( - (nameQueried) => !roles.find(({ name }) => name === nameQueried), - ); - if (adminRoleNameNotFound) { - return new BadRequest400( - `Unable to find adminRole ${adminRoleNameNotFound}`, - ); - } - whereInputs.push({ - adminRoleIds: { hasEvery: roles.map(({ id }) => id) }, - }); - } - if (query.memberOfIds) { - whereInputs.push({ - AND: query.memberOfIds.map((id) => ({ - OR: [ - { projectsOwned: { some: { id } } }, - { ProjectMembers: { some: { project: { id } } } }, - ], - })), - }); - } - - return getUsersQuery({ [relationType]: whereInputs }); +import { createHash } from 'node:crypto' +import type { AdminRole, AdminToken, PersonalAccessToken, Prisma, User } from '@prisma/client' +import type { XOR, userContract } from '@cpn-console/shared' +import { getMatchingUsers as getMatchingUsersQuery, getUsers as getUsersQuery } from '@old-server/resources/queries-index' +import prisma from '@old-server/prisma' +import type { UserDetails } from '@old-server/types/index' +import { BadRequest400 } from '@old-server/utils/errors' + +export async function getUsers(query: typeof userContract.getAllUsers.query._type, relationType: 'OR' | 'AND' = 'AND') { + const whereInputs: Prisma.UserWhereInput[] = [] + if (query.adminRoleIds?.length) { + whereInputs.push({ adminRoleIds: { hasEvery: query.adminRoleIds } }) + } + if (query.adminRoles?.length) { + const roles = query.adminRoles + ? await prisma.adminRole.findMany({ where: { name: { in: query.adminRoles } } }) + : [] + + const adminRoleNameNotFound = query.adminRoles?.find(nameQueried => !roles.find(({ name }) => name === nameQueried)) + if (adminRoleNameNotFound) { + return new BadRequest400(`Unable to find adminRole ${adminRoleNameNotFound}`) + } + whereInputs.push({ adminRoleIds: { hasEvery: roles.map(({ id }) => id) } }) + } + if (query.memberOfIds) { + whereInputs.push({ + AND: query.memberOfIds.map(id => ({ + OR: [ + { projectsOwned: { some: { id } } }, + { ProjectMembers: { some: { project: { id } } } }, + ], + })), + }) + } + + return getUsersQuery({ [relationType]: whereInputs }) } -export async function getMatchingUsers( - query: typeof userContract.getMatchingUsers.query._type, -) { - const AND: Prisma.UserWhereInput[] = []; - if (query.notInProjectId) { - AND.push({ - projectMembers: { none: { projectId: query.notInProjectId } }, - }); - AND.push({ projectsOwned: { none: { id: query.notInProjectId } } }); - } - const filter = { contains: query.letters, mode: 'insensitive' } as const; // Default value: default - if (query.letters) { - AND.push({ - OR: [ - { - email: filter, - }, - { - firstName: filter, - }, - { - lastName: filter, - }, - ], - }); - AND.push({ type: 'human' }); - } - - return getMatchingUsersQuery({ - AND, - }); +export async function getMatchingUsers(query: typeof userContract.getMatchingUsers.query._type) { + const AND: Prisma.UserWhereInput[] = [] + if (query.notInProjectId) { + AND.push({ projectMembers: { none: { projectId: query.notInProjectId } } }) + AND.push({ projectsOwned: { none: { id: query.notInProjectId } } }) + } + const filter = { contains: query.letters, mode: 'insensitive' } as const // Default value: default + if (query.letters) { + AND.push({ + OR: [{ + email: filter, + }, { + firstName: filter, + }, { + lastName: filter, + }], + }) + AND.push({ type: 'human' }) + } + + return getMatchingUsersQuery({ + AND, + }) } -export async function patchUsers( - users: typeof userContract.patchUsers.body._type, -) { - for (const user of users) { - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - adminRoleIds: user.adminRoleIds, - }, - }); - } - - return prisma.user.findMany({ - where: { - id: { in: users.map(({ id }) => id) }, - }, - }); +export async function patchUsers(users: typeof userContract.patchUsers.body._type) { + for (const user of users) { + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + adminRoleIds: user.adminRoleIds, + }, + }) + } + + return prisma.user.findMany({ + where: { + id: { in: users.map(({ id }) => id) }, + }, + }) } export enum TokenInvalidReason { - INACTIVE = 'Not active', - EXPIRED = 'Expired', - NOT_FOUND = 'Not authenticated', + INACTIVE = 'Not active', + EXPIRED = 'Expired', + NOT_FOUND = 'Not authenticated', } -type UserTrial = Omit; -export async function logViaSession({ - id, - email, - groups, - ...user -}: UserTrial): Promise<{ user: User; adminPerms: bigint }> { - let userDb = await prisma.user.findUnique({ - where: { id }, - }); - - if (!userDb) { - userDb = await prisma.user.create({ - data: { email, id, ...user, adminRoleIds: [], type: 'human' }, - }); - } - - const matchingAdminRoles = await prisma.adminRole.findMany({ - where: { - OR: [ - { oidcGroup: { in: groups } }, - { id: { in: userDb.adminRoleIds } }, - ], - }, - }); - - const oidcRoleIds = matchingAdminRoles - .filter(({ oidcGroup }) => oidcGroup && groups.includes(oidcGroup)) - .map(({ id }) => id); - - const nonOidcRoleIds = matchingAdminRoles - .filter( - ({ oidcGroup, id }) => - !oidcGroup && userDb.adminRoleIds.includes(id), - ) - .map(({ id }) => id); - - // On enregistre en bdd uniquement les roles de l'utilisateur - // qui ne viennent pas de keycloak - const updatedUser = await prisma.user - .update({ - where: { id }, - data: { - ...user, - adminRoleIds: nonOidcRoleIds, - lastLogin: new Date().toISOString(), - }, - }) - .then((user) => ({ - ...user, - adminRoleIds: [...user.adminRoleIds, ...oidcRoleIds], - })); - return { - user: updatedUser, - adminPerms: sumAdminPerms(matchingAdminRoles), - }; +type UserTrial = Omit +export async function logViaSession({ id, email, groups, ...user }: UserTrial): Promise<{ user: User, adminPerms: bigint }> { + let userDb = await prisma.user.findUnique({ + where: { id }, + }) + + if (!userDb) { + userDb = await prisma.user.create({ data: { email, id, ...user, adminRoleIds: [], type: 'human' } }) + } + + const matchingAdminRoles = await prisma.adminRole.findMany({ + where: { OR: [{ oidcGroup: { in: groups } }, { id: { in: userDb.adminRoleIds } }] }, + }) + + const oidcRoleIds = matchingAdminRoles + .filter(({ oidcGroup }) => oidcGroup && groups.includes(oidcGroup)) + .map(({ id }) => id) + + const nonOidcRoleIds = matchingAdminRoles + .filter(({ oidcGroup, id }) => !oidcGroup && userDb.adminRoleIds.includes(id)) + .map(({ id }) => id) + + // On enregistre en bdd uniquement les roles de l'utilisateur + // qui ne viennent pas de keycloak + const updatedUser = await prisma.user.update({ where: { id }, data: { ...user, adminRoleIds: nonOidcRoleIds, lastLogin: (new Date()).toISOString() } }) + .then(user => ({ ...user, adminRoleIds: [...user.adminRoleIds, ...oidcRoleIds] })) + return { + user: updatedUser, + adminPerms: sumAdminPerms(matchingAdminRoles), + } } -type UserWithTokenId = Omit & { tokenId: string }; -export async function logViaToken( - pass: string, -): Promise<{ user: UserWithTokenId; adminPerms: bigint } | TokenInvalidReason> { - const passHash = createHash('sha256').update(pass).digest('hex'); - - let token: - | (XOR & { owner: User }) - | TokenInvalidReason - | undefined; - const tokenLoginMethods = [findPersonalAccessToken, findAdminToken]; - for (const tokenLoginMethod of tokenLoginMethods) { - token = await tokenLoginMethod(passHash); - if (token) { - break; - } - } - - if (typeof token === 'string') { - return token; - } - if (!token) { - return TokenInvalidReason.NOT_FOUND; - } - - return { - user: { - ...token.owner, - tokenId: token.id, - }, - adminPerms: - token?.permissions ?? - (await getAdminRolesAndSum(token.owner.adminRoleIds)), - }; +type UserWithTokenId = Omit & { tokenId: string } +export async function logViaToken(pass: string): Promise<({ user: UserWithTokenId, adminPerms: bigint }) | TokenInvalidReason> { + const passHash = createHash('sha256').update(pass).digest('hex') + + let token: (XOR & { owner: User }) | TokenInvalidReason | undefined + const tokenLoginMethods = [findPersonalAccessToken, findAdminToken] + for (const tokenLoginMethod of tokenLoginMethods) { + token = await tokenLoginMethod(passHash) + if (token) { + break + } + } + + if (typeof token === 'string') { + return token + } + if (!token) { + return TokenInvalidReason.NOT_FOUND + } + + return { + user: { + ...token.owner, + tokenId: token.id, + }, + adminPerms: token?.permissions ?? await getAdminRolesAndSum(token.owner.adminRoleIds), + } } -function isTokenInvalid( - token: AdminToken | PersonalAccessToken, -): TokenInvalidReason | undefined { - if (token.status !== 'active') { - return TokenInvalidReason.INACTIVE; - } - const currentDate = new Date(); - if ( - token.expirationDate && - currentDate.getTime() > token.expirationDate?.getTime() - ) { - return TokenInvalidReason.EXPIRED; - } +function isTokenInvalid(token: AdminToken | PersonalAccessToken): TokenInvalidReason | undefined { + if (token.status !== 'active') { + return TokenInvalidReason.INACTIVE + } + const currentDate = new Date() + if (token.expirationDate && currentDate.getTime() > token.expirationDate?.getTime()) { + return TokenInvalidReason.EXPIRED + } } function sumAdminPerms(roles: AdminRole[]): bigint { - if (!roles.length) { - return 0n; - } - return roles.reduce((acc, curr) => acc | curr.permissions, 0n); + if (!roles.length) { + return 0n + } + return roles.reduce((acc, curr) => acc | curr.permissions, 0n) } -async function getAdminRolesAndSum( - roles: AdminRole['id'][] | null, -): Promise { - if (!roles?.length) { - return 0n; - } - return sumAdminPerms( - await prisma.adminRole.findMany({ - where: { id: { in: roles } }, - }), - ); +async function getAdminRolesAndSum(roles: AdminRole['id'][] | null): Promise { + if (!roles?.length) { + return 0n + } + return sumAdminPerms(await prisma.adminRole.findMany({ + where: { id: { in: roles } }, + })) } // List all token tpe authentication -async function findPersonalAccessToken( - digest: string, -): Promise< - (PersonalAccessToken & { owner: User }) | undefined | TokenInvalidReason -> { - const token = await prisma.personalAccessToken.findFirst({ - where: { hash: digest }, - include: { owner: true }, - }); - if (!token) return undefined; - const invalidReason = isTokenInvalid(token); - if (invalidReason) { - return invalidReason; - } - await prisma.personalAccessToken.update({ - where: { id: token.id }, - data: { lastUse: new Date().toISOString() }, - }); - await prisma.user.update({ - where: { id: token.owner.id }, - data: { lastLogin: new Date().toISOString() }, - }); - return token; +async function findPersonalAccessToken(digest: string): Promise<(PersonalAccessToken & { owner: User }) | undefined | TokenInvalidReason> { + const token = await prisma.personalAccessToken.findFirst({ where: { hash: digest }, include: { owner: true } }) + if (!token) + return undefined + const invalidReason = isTokenInvalid(token) + if (invalidReason) { + return invalidReason + } + await prisma.personalAccessToken.update({ where: { id: token.id }, data: { lastUse: (new Date()).toISOString() } }) + await prisma.user.update({ where: { id: token.owner.id }, data: { lastLogin: (new Date()).toISOString() } }) + return token } -async function findAdminToken( - digest: string, -): Promise<(AdminToken & { owner: User }) | undefined | TokenInvalidReason> { - const token = await prisma.adminToken.findFirst({ - where: { hash: digest }, - include: { owner: true }, - }); - if (!token) return undefined; - const invalidReason = isTokenInvalid(token); - if (invalidReason) { - return invalidReason; - } - await prisma.adminToken.update({ - where: { id: token.id }, - data: { lastUse: new Date().toISOString() }, - }); - await prisma.user.update({ - where: { id: token.userId }, - data: { lastLogin: new Date().toISOString() }, - }); - return token; +async function findAdminToken(digest: string): Promise<(AdminToken & { owner: User }) | undefined | TokenInvalidReason> { + const token = await prisma.adminToken.findFirst({ where: { hash: digest }, include: { owner: true } }) + if (!token) + return undefined + const invalidReason = isTokenInvalid(token) + if (invalidReason) { + return invalidReason + } + await prisma.adminToken.update({ where: { id: token.id }, data: { lastUse: (new Date()).toISOString() } }) + await prisma.user.update({ where: { id: token.userId }, data: { lastLogin: (new Date()).toISOString() } }) + return token } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts index 0533a5001..414b71abc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts @@ -1,83 +1,60 @@ -import prisma from '@old-server/prisma'; -import type { Prisma, User } from '@prisma/client'; +import type { Prisma, User } from '@prisma/client' +import prisma from '@old-server/prisma' -type UserCreate = Omit; +type UserCreate = Omit // SELECT -export const getUsers = (where?: Prisma.UserWhereInput) => - prisma.user.findMany({ where }); +export const getUsers = (where?: Prisma.UserWhereInput) => prisma.user.findMany({ where }) export async function getUserInfos(id: User['id']) { - return prisma.user.findMany({ - where: { id }, - include: { - logs: true, - }, - }); + return prisma.user.findMany({ + where: { id }, + include: { + logs: true, + }, + }) } export function getMatchingUsers(where: Prisma.UserWhereInput) { - return prisma.user.findMany({ - where, - take: 5, - }); + return prisma.user.findMany({ + where, + take: 5, + }) } export function getUserById(id: User['id']) { - return prisma.user.findUnique({ where: { id } }); + return prisma.user.findUnique({ where: { id } }) } export function getUserOrThrow(id: User['id']) { - return prisma.user.findUniqueOrThrow({ - where: { id }, - }); + return prisma.user.findUniqueOrThrow({ + where: { id }, + }) } export function getUserByEmail(email: User['email']) { - return prisma.user.findUnique({ where: { email } }); + return prisma.user.findUnique({ where: { email } }) } // CREATE -export async function createUser({ - id, - email, - firstName, - lastName, - type, -}: UserCreate) { - const user = await getUserByEmail(email); - if (user) - throw new Error('Un utilisateur avec cette adresse e-mail existe déjà'); - return prisma.user.create({ - data: { id, email, firstName, lastName, type }, - }); +export async function createUser({ id, email, firstName, lastName, type }: UserCreate) { + const user = await getUserByEmail(email) + if (user) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') + return prisma.user.create({ data: { id, email, firstName, lastName, type } }) } // UPDATE -export async function updateUserById({ - id, - email, - firstName, - lastName, -}: UserCreate) { - const user = await getUserById(id); - const isEmailAlreadyTaken = await getUserByEmail(email); - if (!user) throw new Error("L'utilisateur demandé n'existe pas"); - if (isEmailAlreadyTaken) - throw new Error('Un utilisateur avec cette adresse e-mail existe déjà'); - if (user && !isEmailAlreadyTaken) { - return prisma.user.update({ - where: { id }, - data: { email, firstName, lastName }, - }); - } +export async function updateUserById({ id, email, firstName, lastName }: UserCreate) { + const user = await getUserById(id) + const isEmailAlreadyTaken = await getUserByEmail(email) + if (!user) throw new Error('L\'utilisateur demandé n\'existe pas') + if (isEmailAlreadyTaken) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') + if (user && !isEmailAlreadyTaken) { + return prisma.user.update({ where: { id }, data: { email, firstName, lastName } }) + } } // TECH export function _createUser(data: Prisma.UserCreateInput) { - return prisma.user.upsert({ - where: { id: data.id }, - create: data, - update: data, - }); + return prisma.user.upsert({ where: { id: data.id }, create: data, update: data }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts index 9cd9ec655..5768e869e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts @@ -1,156 +1,139 @@ -import { userContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { getUserMockInfos, setRequestor } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessGetMatchingMock = vi.spyOn(business, 'getMatchingUsers'); -const businessLogViaSessionMock = vi.spyOn(business, 'logViaSession'); -const businessGetUsersMock = vi.spyOn(business, 'getUsers'); -const businessPatchMock = vi.spyOn(business, 'patchUsers'); +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { userContract } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getUserMockInfos, setRequestor } from '../../utils/mocks' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessGetMatchingMock = vi.spyOn(business, 'getMatchingUsers') +const businessLogViaSessionMock = vi.spyOn(business, 'logViaSession') +const businessGetUsersMock = vi.spyOn(business, 'getUsers') +const businessPatchMock = vi.spyOn(business, 'patchUsers') describe('test userContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('getMatchingUsers', () => { - it('should return matching users', async () => { - const usersMatching = []; - businessGetMatchingMock.mockResolvedValueOnce(usersMatching); - - const response = await app - .inject() - .get(userContract.getMatchingUsers.path) - .query({ letters: faker.person.fullName() }) - .end(); - - expect(businessGetMatchingMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(usersMatching); - expect(response.statusCode).toEqual(200); - }); - }); - - describe('auth', () => { - it('should return logged user', async () => { - const user = { - id: faker.string.uuid(), - adminRoleIds: [], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - type: 'human', - lastName: faker.person.lastName(), - }; - setRequestor(user); - businessLogViaSessionMock.mockResolvedValueOnce({ - user, - adminPerms: 0n, - }); - - const response = await app - .inject() - .get(userContract.auth.path) - .end(); - - expect(businessLogViaSessionMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(user); - expect(response.statusCode).toEqual(200); - }); - }); - - describe('getAllUsers', () => { - it('should return all users for admin', async () => { - const user = getUserMockInfos(true); - const users = []; - authUserMock.mockResolvedValueOnce(user); - businessGetUsersMock.mockResolvedValueOnce(users); - - const response = await app - .inject() - .get(userContract.getAllUsers.path) - .query({ role: 'admin' }) - .end(); - - expect(authUserMock).toHaveBeenCalledTimes(1); - expect(businessGetUsersMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(users); - expect(response.statusCode).toEqual(200); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .get(userContract.getAllUsers.path) - .query({ role: 'admin' }) - .end(); - - expect(authUserMock).toHaveBeenCalledTimes(1); - expect(businessGetUsersMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('patchUsers', () => { - const usersPatchData = [ - { - id: faker.string.uuid(), - adminRoleIds: [], - }, - ]; - const usersReturn = [ - { - id: faker.string.uuid(), - adminRoleIds: [], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - type: 'human', - }, - ]; - - it('should patch and return users for admin', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessPatchMock.mockResolvedValueOnce(usersReturn); - const response = await app - .inject() - .patch(userContract.patchUsers.path) - .body(usersPatchData) - .end(); - - expect(authUserMock).toHaveBeenCalledTimes(1); - expect(businessPatchMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(usersReturn); - expect(response.statusCode).toEqual(200); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .patch(userContract.patchUsers.path) - .body(usersPatchData) - .end(); - - expect(authUserMock).toHaveBeenCalledTimes(1); - expect(businessPatchMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('getMatchingUsers', () => { + it('should return matching users', async () => { + const usersMatching = [] + businessGetMatchingMock.mockResolvedValueOnce(usersMatching) + + const response = await app.inject() + .get(userContract.getMatchingUsers.path) + .query({ letters: faker.person.fullName() }) + .end() + + expect(businessGetMatchingMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(usersMatching) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('auth', () => { + it('should return logged user', async () => { + const user = { + id: faker.string.uuid(), + adminRoleIds: [], + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + type: 'human', + lastName: faker.person.lastName(), + } + setRequestor(user) + businessLogViaSessionMock.mockResolvedValueOnce({ user, adminPerms: 0n }) + + const response = await app.inject() + .get(userContract.auth.path) + .end() + + expect(businessLogViaSessionMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(user) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('getAllUsers', () => { + it('should return all users for admin', async () => { + const user = getUserMockInfos(true) + const users = [] + authUserMock.mockResolvedValueOnce(user) + businessGetUsersMock.mockResolvedValueOnce(users) + + const response = await app.inject() + .get(userContract.getAllUsers.path) + .query({ role: 'admin' }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetUsersMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(users) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .get(userContract.getAllUsers.path) + .query({ role: 'admin' }) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessGetUsersMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('patchUsers', () => { + const usersPatchData = [{ + id: faker.string.uuid(), + adminRoleIds: [], + }] + const usersReturn = [{ + id: faker.string.uuid(), + adminRoleIds: [], + createdAt: (new Date()).toISOString(), + updatedAt: (new Date()).toISOString(), + email: faker.internet.email(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + type: 'human', + }] + + it('should patch and return users for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessPatchMock.mockResolvedValueOnce(usersReturn) + const response = await app.inject() + .patch(userContract.patchUsers.path) + .body(usersPatchData) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessPatchMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(usersReturn) + expect(response.statusCode).toEqual(200) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .patch(userContract.patchUsers.path) + .body(usersPatchData) + .end() + + expect(authUserMock).toHaveBeenCalledTimes(1) + expect(businessPatchMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts index e664a22a7..42e03caef 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts @@ -1,79 +1,63 @@ -import { AdminAuthorized, userContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import '@old-server/types/index'; -import { authUser } from '@old-server/utils/controller'; +import { AdminAuthorized, userContract } from '@cpn-console/shared' import { - ErrorResType, - Forbidden403, - Unauthorized401, -} from '@old-server/utils/errors'; - -import { - getMatchingUsers, - getUsers, - logViaSession, - patchUsers, -} from './business'; - -@Injectable() -export class UserRouterService { - constructor(private readonly appService: AppService) {} - - userRouter() { - return this.appService.serverInstance.router(userContract, { - getMatchingUsers: async ({ query }) => { - const usersMatching = await getMatchingUsers(query); - - return { - status: 200, - body: usersMatching, - }; - }, - - auth: async ({ request: req }) => { - const user = req.session.user; - - if (!user) return new Unauthorized401(); - - const { user: body } = await logViaSession(user); - - return { - status: 200, - body, - }; - }, - - getAllUsers: async ({ - request: req, - query: { relationType, ...query }, - }) => { - const perms = await authUser(req); - - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const body = await getUsers(query, relationType); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - patchUsers: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const users = await patchUsers(body); - - return { - status: 200, - body: users, - }; - }, - }); - } + getMatchingUsers, + getUsers, + logViaSession, + patchUsers, +} from './business' +import '@old-server/types/index' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' + +export function userRouter() { + return serverInstance.router(userContract, { + getMatchingUsers: async ({ query }) => { + const usersMatching = await getMatchingUsers(query) + + return { + status: 200, + body: usersMatching, + } + }, + + auth: async ({ request: req }) => { + const user = req.session.user + + if (!user) return new Unauthorized401() + + const { user: body } = await logViaSession(user) + + return { + status: 200, + body, + } + }, + + getAllUsers: async ({ request: req, query: { relationType, ...query } }) => { + const perms = await authUser(req) + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const body = await getUsers(query, relationType) + if (body instanceof ErrorResType) return body + + return { + status: 200, + body, + } + }, + + patchUsers: async ({ request: req, body }) => { + const perms = await authUser(req) + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + const users = await patchUsers(body) + + return { + status: 200, + body: users, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts index 9ba5201f3..5d7277526 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts @@ -1,61 +1,51 @@ -import type { personalAccessTokenContract } from '@cpn-console/shared'; -import { generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared'; -import { BadRequest400 } from '@old-server/utils/errors'; -import type { AdminToken, User } from '@prisma/client'; -import { createHash } from 'node:crypto'; - -import prisma from '../../../prisma'; +import { createHash } from 'node:crypto' +import type { personalAccessTokenContract } from '@cpn-console/shared' +import { generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' +import type { AdminToken, User } from '@prisma/client' +import prisma from '../../../prisma' +import { BadRequest400 } from '@old-server/utils/errors' export async function listTokens(userId: User['id']) { - return prisma.personalAccessToken.findMany({ - omit: { hash: true }, - include: { owner: true }, - orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], - where: { userId }, - }); + return prisma.personalAccessToken.findMany({ + omit: { hash: true }, + include: { owner: true }, + orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], + where: { userId }, + }) } -export async function createToken( - data: typeof personalAccessTokenContract.createPersonalAccessToken.body._type, - userId: User['id'], -) { - if ( - data.expirationDate && - !isAtLeastTomorrow(new Date(data.expirationDate)) - ) { - return new BadRequest400("Date d'expiration trop courte"); - } - const password = generateRandomPassword( - 48, - 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-', - ); - const hash = createHash('sha256').update(password).digest('hex'); - const token = await prisma.personalAccessToken.create({ - data: { - ...data, - hash, - expirationDate: new Date(data.expirationDate), - userId, - }, - omit: { hash: true }, - include: { owner: true }, - }); - return { - ...token, - password, - }; +export async function createToken(data: typeof personalAccessTokenContract.createPersonalAccessToken.body._type, userId: User['id']) { + if (data.expirationDate && !isAtLeastTomorrow(new Date(data.expirationDate))) { + return new BadRequest400('Date d\'expiration trop courte') + } + const password = generateRandomPassword(48, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') + const hash = createHash('sha256').update(password).digest('hex') + const token = await prisma.personalAccessToken.create({ + data: { + ...data, + hash, + expirationDate: new Date(data.expirationDate), + userId, + }, + omit: { hash: true }, + include: { owner: true }, + }) + return { + ...token, + password, + } } export async function deleteToken(id: AdminToken['id'], userId: User['id']) { - const token = await prisma.personalAccessToken.findUnique({ - where: { - id, - userId, - }, - }); - if (token) { - return prisma.personalAccessToken.delete({ - where: { id }, - }); - } + const token = await prisma.personalAccessToken.findUnique({ + where: { + id, + userId, + }, + }) + if (token) { + return prisma.personalAccessToken.delete({ + where: { id }, + }) + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts index 6606fd495..bc320b6d1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts @@ -1,63 +1,48 @@ -import { personalAccessTokenContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import '@old-server/types/index'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - -import { createToken, deleteToken, listTokens } from './business'; - -@Injectable() -export class UserTokensRouterService { - constructor(private readonly appService: AppService) {} - - personalAccessTokenRouter() { - return this.appService.serverInstance.router( - personalAccessTokenContract, - { - listPersonalAccessTokens: async ({ request: req }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - const body = await listTokens(perms.user.id); - - return { - status: 200, - body, - }; - }, - - createPersonalAccessToken: async ({ - request: req, - body: data, - }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - const body = await createToken(data, perms.user.id); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - deletePersonalAccessToken: async ({ request: req, params }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - await deleteToken(params.tokenId, perms.user.id); - - return { - status: 204, - body: null, - }; - }, - }, - ); - } +import { personalAccessTokenContract } from '@cpn-console/shared' + +import '@old-server/types/index' +import { createToken, deleteToken, listTokens } from './business' +import { serverInstance } from '@old-server/app' +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' + +export function personalAccessTokenRouter() { + return serverInstance.router(personalAccessTokenContract, { + listPersonalAccessTokens: async ({ request: req }) => { + const perms = await authUser(req) + + if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + const body = await listTokens(perms.user.id) + + return { + status: 200, + body, + } + }, + + createPersonalAccessToken: async ({ request: req, body: data }) => { + const perms = await authUser(req) + + if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + const body = await createToken(data, perms.user.id) + if (body instanceof ErrorResType) return body + + return { + status: 201, + body, + } + }, + + deletePersonalAccessToken: async ({ request: req, params }) => { + const perms = await authUser(req) + + if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + await deleteToken(params.tokenId, perms.user.id) + + return { + status: 204, + body: null, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts index f51967e21..938658e08 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts @@ -1,159 +1,133 @@ -import { faker } from '@faker-js/faker'; -import type { Cluster, Zone } from '@prisma/client'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Cluster, Zone } from '@prisma/client' +import prisma from '../../__mocks__/prisma' +import { BadRequest400 } from '../../utils/errors' +import { hook } from '../../__mocks__/utils/hook-wrapper' +import { createZone, deleteZone, listZones, updateZone } from './business' +import * as queries from './queries' -import prisma from '../../__mocks__/prisma'; -import { hook } from '../../__mocks__/utils/hook-wrapper'; -import { BadRequest400 } from '../../utils/errors'; -import { createZone, deleteZone, listZones, updateZone } from './business'; -import * as queries from './queries'; - -const userId = faker.string.uuid(); -const reqId = faker.string.uuid(); -const linkZoneToClustersMock = vi.spyOn(queries, 'linkZoneToClusters'); +const userId = faker.string.uuid() +const reqId = faker.string.uuid() +const linkZoneToClustersMock = vi.spyOn(queries, 'linkZoneToClusters') vi.mock('../../utils/hook-wrapper', async () => ({ - hook, -})); + hook, +})) describe('test zone business', () => { - const zones: Zone[] = [ - { - id: faker.string.uuid(), - label: faker.company.name(), - argocdUrl: faker.internet.url(), - createdAt: new Date(), - updatedAt: new Date(), - description: faker.lorem.lines(1), - slug: faker.string.alphanumeric(5), - }, - { - id: faker.string.uuid(), - label: faker.company.name(), - argocdUrl: faker.internet.url(), - createdAt: new Date(), - updatedAt: new Date(), - description: faker.lorem.lines(1), - slug: faker.string.alphanumeric(6), - }, - ]; + const zones: Zone[] = [{ + id: faker.string.uuid(), + label: faker.company.name(), + argocdUrl: faker.internet.url(), + createdAt: new Date(), + updatedAt: new Date(), + description: faker.lorem.lines(1), + slug: faker.string.alphanumeric(5), + }, { + id: faker.string.uuid(), + label: faker.company.name(), + argocdUrl: faker.internet.url(), + createdAt: new Date(), + updatedAt: new Date(), + description: faker.lorem.lines(1), + slug: faker.string.alphanumeric(6), + }] - const clusters: Pick[] = [ - { id: faker.string.uuid() }, - { id: faker.string.uuid() }, - ]; + const clusters: Pick[] = [ + { id: faker.string.uuid() }, + { id: faker.string.uuid() }, + ] - beforeEach(() => { - vi.resetAllMocks(); - }); - describe('listZones', () => { - it('should return zones', async () => { - prisma.zone.findMany.mockResolvedValueOnce(zones); + beforeEach(() => { + vi.resetAllMocks() + }) + describe('listZones', () => { + it('should return zones', async () => { + prisma.zone.findMany.mockResolvedValueOnce(zones) - const response = await listZones(); - expect(response).toEqual(zones); - }); - }); - describe('createZone', () => { - it('should create zone without description and clusterIds', async () => { - const newZone = { - label: zones[0].label, - slug: zones[0].slug, - argocdUrl: zones[0].argocdUrl, - }; + const response = await listZones() + expect(response).toEqual(zones) + }) + }) + describe('createZone', () => { + it('should create zone without description and clusterIds', async () => { + const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl } - hook.zone.upsert.mockResolvedValue({}); - prisma.zone.create.mockResolvedValueOnce(zones[0]); - const response = await createZone(newZone, userId, reqId); + hook.zone.upsert.mockResolvedValue({}) + prisma.zone.create.mockResolvedValueOnce(zones[0]) + const response = await createZone(newZone, userId, reqId) - expect(response).toEqual(zones[0]); - expect(prisma.zone.create).toHaveBeenCalledWith({ - data: { - slug: newZone.slug, - label: newZone.label, - argocdUrl: newZone.argocdUrl, - description: undefined, - }, - }); - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0); - }); - it('should create zone with description and clusterIds', async () => { - const newZone = { - label: zones[0].label, - slug: zones[0].slug, - argocdUrl: zones[0].argocdUrl, - clusterIds: clusters.map(({ id }) => id), - description: faker.lorem.lines(2), - }; + expect(response).toEqual(zones[0]) + expect(prisma.zone.create).toHaveBeenCalledWith({ + data: { + slug: newZone.slug, + label: newZone.label, + argocdUrl: newZone.argocdUrl, + description: undefined, + }, + }) + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0) + }) + it('should create zone with description and clusterIds', async () => { + const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl, clusterIds: clusters.map(({ id }) => id), description: faker.lorem.lines(2) } - hook.zone.upsert.mockResolvedValue({}); - prisma.zone.create.mockResolvedValueOnce(zones[0]); - const response = await createZone(newZone, userId, reqId); + hook.zone.upsert.mockResolvedValue({}) + prisma.zone.create.mockResolvedValueOnce(zones[0]) + const response = await createZone(newZone, userId, reqId) - expect(response).toEqual(zones[0]); - expect(prisma.zone.create).toHaveBeenCalledWith({ - data: { - description: newZone.description, - label: newZone.label, - argocdUrl: newZone.argocdUrl, - slug: newZone.slug, - }, - }); - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(1); - }); - it('should not create zone, conflict label', async () => { - const newZone = { - label: zones[0].label, - slug: zones[0].slug, - argocdUrl: zones[0].argocdUrl, - }; + expect(response).toEqual(zones[0]) + expect(prisma.zone.create).toHaveBeenCalledWith({ + data: { + description: newZone.description, + label: newZone.label, + argocdUrl: newZone.argocdUrl, + slug: newZone.slug, + }, + }) + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(1) + }) + it('should not create zone, conflict label', async () => { + const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl } - prisma.zone.findUnique.mockResolvedValueOnce(zones[0]); - prisma.zone.create.mockResolvedValueOnce(zones[0]); - const response = await createZone(newZone, userId, reqId); + prisma.zone.findUnique.mockResolvedValueOnce(zones[0]) + prisma.zone.create.mockResolvedValueOnce(zones[0]) + const response = await createZone(newZone, userId, reqId) - expect(response).instanceOf(BadRequest400); - expect(prisma.zone.create).toHaveBeenCalledTimes(0); - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0); - }); - }); - describe('updateZone', () => { - it('should filter keys and update zone', async () => { - prisma.zone.update.mockResolvedValueOnce(zones[0]); - hook.zone.upsert.mockResolvedValue({}); - await updateZone( - zones[0].id, - { - description: '', - label: zones[0].label, - argocdUrl: zones[0].argocdUrl, - extraKey: 1, - }, - userId, - reqId, - ); - expect(prisma.zone.update).toHaveBeenCalledWith({ - where: { id: zones[0].id }, - data: { - description: '', - label: zones[0].label, - argocdUrl: zones[0].argocdUrl, - }, - }); - }); - }); - describe('deleteZone', () => { - it('should not delete zone, cluster attached', async () => { - prisma.cluster.findFirst.mockResolvedValueOnce(clusters[0]); - const response = await deleteZone(zones[0].id, userId, reqId); - expect(response).instanceOf(BadRequest400); - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0); - }); - it('should delete zone', async () => { - prisma.cluster.findFirst.mockResolvedValueOnce(undefined); - hook.zone.delete.mockResolvedValue({}); - const response = await deleteZone(zones[0].id, userId, reqId); - expect(response).toEqual(null); - expect(prisma.zone.delete).toHaveBeenCalledTimes(1); - }); - }); -}); + expect(response).instanceOf(BadRequest400) + expect(prisma.zone.create).toHaveBeenCalledTimes(0) + expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0) + }) + }) + describe('updateZone', () => { + it('should filter keys and update zone', async () => { + prisma.zone.update.mockResolvedValueOnce(zones[0]) + hook.zone.upsert.mockResolvedValue({}) + await updateZone(zones[0].id, { + description: '', + label: zones[0].label, + argocdUrl: zones[0].argocdUrl, + extraKey: 1, + }, userId, reqId) + expect(prisma.zone.update).toHaveBeenCalledWith({ where: { id: zones[0].id }, data: { + description: '', + label: zones[0].label, + argocdUrl: zones[0].argocdUrl, + } }) + }) + }) + describe('deleteZone', () => { + it('should not delete zone, cluster attached', async () => { + prisma.cluster.findFirst.mockResolvedValueOnce(clusters[0]) + const response = await deleteZone(zones[0].id, userId, reqId) + expect(response).instanceOf(BadRequest400) + expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) + }) + it('should delete zone', async () => { + prisma.cluster.findFirst.mockResolvedValueOnce(undefined) + hook.zone.delete.mockResolvedValue({}) + const response = await deleteZone(zones[0].id, userId, reqId) + expect(response).toEqual(null) + expect(prisma.zone.delete).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts index 9201d0554..293dc8675 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts @@ -1,119 +1,78 @@ -import type { User, Zone } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; -import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors'; -import { hook } from '@old-server/utils/hook-wrapper'; +import type { User, Zone } from '@cpn-console/shared' +import { addLogs } from '../queries-index' +import { linkZoneToClusters } from './queries' +import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors' +import prisma from '@old-server/prisma' +import { hook } from '@old-server/utils/hook-wrapper' -import { addLogs } from '../queries-index'; -import { linkZoneToClusters } from './queries'; - -export const listZones = prisma.zone.findMany; +export const listZones = prisma.zone.findMany export async function createZone( - data: { - slug: string; - label: string; - argocdUrl: string; - description?: string | null; - clusterIds?: string[]; - }, - userId: User['id'], - requestId: string, + data: { slug: string, label: string, argocdUrl: string, description?: string | null, clusterIds?: string[] }, + userId: User['id'], + requestId: string, ) { - const { slug, label, argocdUrl, description, clusterIds } = data; + const { slug, label, argocdUrl, description, clusterIds } = data - const existingZone = await prisma.zone.findUnique({ - where: { slug }, - }); + const existingZone = await prisma.zone.findUnique({ + where: { slug }, + }) - if (existingZone) - return new BadRequest400( - `Une zone portant le nom ${slug} existe déjà.`, - ); - const zone = await prisma.zone.create({ - data: { - slug, - label, - argocdUrl, - description, - }, - }); - if (clusterIds) { - await linkZoneToClusters(zone.id, clusterIds); - } - const hookReply = await hook.zone.upsert(zone.id); - await addLogs({ - action: 'Create zone', - data: hookReply, - userId, - requestId, - }); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services lors de la création de la zone', - ); - } - return zone; + if (existingZone) return new BadRequest400(`Une zone portant le nom ${slug} existe déjà.`) + const zone = await prisma.zone.create({ + data: { + slug, + label, + argocdUrl, + description, + }, + }) + if (clusterIds) { + await linkZoneToClusters(zone.id, clusterIds) + } + const hookReply = await hook.zone.upsert(zone.id) + await addLogs({ action: 'Create zone', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services lors de la création de la zone') + } + return zone } export async function updateZone( - zoneId: Zone['id'], - data: Pick, - userId: User['id'], - requestId: string, + zoneId: Zone['id'], + data: Pick, + userId: User['id'], + requestId: string, ) { - const { label, argocdUrl, description } = data; + const { label, argocdUrl, description } = data - const updatedZone = await prisma.zone.update({ - where: { - id: zoneId, - }, - data: { - label, - argocdUrl, - description, - }, - }); - const hookReply = await hook.zone.upsert(updatedZone.id); - await addLogs({ - action: 'Update zone', - data: hookReply, - userId, - requestId, - }); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services lors de la mise à jour de la zone', - ); - } - return updatedZone; + const updatedZone = await prisma.zone.update({ + where: { + id: zoneId, + }, + data: { + label, + argocdUrl, + description, + }, + }) + const hookReply = await hook.zone.upsert(updatedZone.id) + await addLogs({ action: 'Update zone', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services lors de la mise à jour de la zone') + } + return updatedZone } -export async function deleteZone( - zoneId: Zone['id'], - userId: User['id'], - requestId: string, -) { - const attachedCluster = await prisma.cluster.findFirst({ - where: { zoneId }, - select: { id: true }, - }); - if (attachedCluster) - return new BadRequest400( - 'Vous ne pouvez supprimer cette zone, car des clusters y sont associés.', - ); +export async function deleteZone(zoneId: Zone['id'], userId: User['id'], requestId: string) { + const attachedCluster = await prisma.cluster.findFirst({ where: { zoneId }, select: { id: true } }) + if (attachedCluster) return new BadRequest400('Vous ne pouvez supprimer cette zone, car des clusters y sont associés.') - const hookReply = await hook.zone.delete(zoneId); - await addLogs({ - action: 'Delete zone', - data: hookReply, - userId, - requestId, - }); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services lors de la suppression de la zone', - ); - } - await prisma.zone.delete({ where: { id: zoneId } }); - return null; + const hookReply = await hook.zone.delete(zoneId) + await addLogs({ action: 'Delete zone', data: hookReply, userId, requestId }) + if (hookReply.failed) { + return new Unprocessable422('Echec des services lors de la suppression de la zone') + } + await prisma.zone.delete({ where: { id: zoneId } }) + return null } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts index 6295fcdc0..9f3f7b355 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts @@ -1,24 +1,21 @@ -import prisma from '@old-server/prisma'; -import type { Cluster, Zone } from '@prisma/client'; +import type { Cluster, Zone } from '@prisma/client' +import prisma from '@old-server/prisma' export function getZoneByIdOrThrow(id: Zone['id']) { - return prisma.zone.findUniqueOrThrow({ - where: { id }, - }); + return prisma.zone.findUniqueOrThrow({ + where: { id }, + }) } -export function linkZoneToClusters( - zoneId: Zone['id'], - clusterIds: Cluster['id'][], -) { - return prisma.zone.update({ - where: { - id: zoneId, - }, - data: { - clusters: { - connect: clusterIds.map((clusterId) => ({ id: clusterId })), - }, - }, - }); +export function linkZoneToClusters(zoneId: Zone['id'], clusterIds: Cluster['id'][]) { + return prisma.zone.update({ + where: { + id: zoneId, + }, + data: { + clusters: { + connect: clusterIds.map(clusterId => ({ id: clusterId })), + }, + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts index 9218a0ba8..0bf4df4d4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts @@ -1,208 +1,162 @@ -import type { Zone } from '@cpn-console/shared'; -import { zoneContract } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import app from '../../app'; -import * as utilsController from '../../utils/controller'; -import { BadRequest400 } from '../../utils/errors'; -import { getUserMockInfos } from '../../utils/mocks'; -import * as business from './business'; - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -); -const authUserMock = vi.spyOn(utilsController, 'authUser'); -const businessListMock = vi.spyOn(business, 'listZones'); -const businessCreateMock = vi.spyOn(business, 'createZone'); -const businessUpdateMock = vi.spyOn(business, 'updateZone'); -const businessDeleteMock = vi.spyOn(business, 'deleteZone'); +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Zone } from '@cpn-console/shared' +import { zoneContract } from '@cpn-console/shared' +import app from '../../app' +import * as utilsController from '../../utils/controller' +import { getUserMockInfos } from '../../utils/mocks' +import { BadRequest400 } from '../../utils/errors' +import * as business from './business' + +vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) +const authUserMock = vi.spyOn(utilsController, 'authUser') +const businessListMock = vi.spyOn(business, 'listZones') +const businessCreateMock = vi.spyOn(business, 'createZone') +const businessUpdateMock = vi.spyOn(business, 'updateZone') +const businessDeleteMock = vi.spyOn(business, 'deleteZone') describe('test zoneContract', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - describe('listZones', () => { - it('should return list of zones', async () => { - const zones = []; - businessListMock.mockResolvedValueOnce(zones); - - const response = await app - .inject() - .get(zoneContract.listZones.path) - .end(); - - expect(businessListMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(zones); - expect(response.statusCode).toEqual(200); - }); - }); - - describe('createZone', () => { - const zone = { - id: faker.string.uuid(), - label: faker.string.alpha({ length: 5 }), - argocdUrl: faker.internet.url(), - slug: faker.string.alpha({ length: 5, casing: 'lower' }), - description: '', - }; - - it('should create and return zone for admin', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce(zone); - const response = await app - .inject() - .post(zoneContract.createZone.path) - .body(zone) - .end(); - - expect(businessCreateMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual(zone); - expect(response.statusCode).toEqual(201); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessCreateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .post(zoneContract.createZone.path) - .body(zone) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .post(zoneContract.createZone.path) - .body(zone) - .end(); - - expect(businessCreateMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('updateZone', () => { - const zoneId = faker.string.uuid(); - const zone: Omit = { - label: faker.string.alpha({ length: 5 }), - slug: faker.string.alpha({ length: 5, casing: 'lower' }), - argocdUrl: faker.internet.url(), - description: '', - }; - - it('should update and return zone for admin', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce({ id: zoneId, ...zone }); - const response = await app - .inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledTimes(1); - expect(response.json()).toEqual({ id: zoneId, ...zone }); - expect(response.statusCode).toEqual(200); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessUpdateMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end(); - - expect(businessUpdateMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); - - describe('deleteZone', () => { - it('should delete zone for admin', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce(null); - const response = await app - .inject() - .delete( - zoneContract.deleteZone.path.replace( - ':zoneId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessDeleteMock).toHaveBeenCalledTimes(1); - expect(response.body).toBeFalsy(); - expect(response.statusCode).toEqual(204); - }); - it('should pass business error', async () => { - const user = getUserMockInfos(true); - authUserMock.mockResolvedValueOnce(user); - - businessDeleteMock.mockResolvedValueOnce( - new BadRequest400('une erreur'), - ); - const response = await app - .inject() - .delete( - zoneContract.deleteZone.path.replace( - ':zoneId', - faker.string.uuid(), - ), - ) - .end(); - - expect(response.statusCode).toEqual(400); - }); - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false); - authUserMock.mockResolvedValueOnce(user); - - const response = await app - .inject() - .delete( - zoneContract.deleteZone.path.replace( - ':zoneId', - faker.string.uuid(), - ), - ) - .end(); - - expect(businessDeleteMock).toHaveBeenCalledTimes(0); - expect(response.statusCode).toEqual(403); - }); - }); -}); + beforeEach(() => { + vi.resetAllMocks() + }) + + describe('listZones', () => { + it('should return list of zones', async () => { + const zones = [] + businessListMock.mockResolvedValueOnce(zones) + + const response = await app.inject() + .get(zoneContract.listZones.path) + .end() + + expect(businessListMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(zones) + expect(response.statusCode).toEqual(200) + }) + }) + + describe('createZone', () => { + const zone = { id: faker.string.uuid(), label: faker.string.alpha({ length: 5 }), argocdUrl: faker.internet.url(), slug: faker.string.alpha({ length: 5, casing: 'lower' }), description: '' } + + it('should create and return zone for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(zone) + const response = await app.inject() + .post(zoneContract.createZone.path) + .body(zone) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual(zone) + expect(response.statusCode).toEqual(201) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .post(zoneContract.createZone.path) + .body(zone) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .post(zoneContract.createZone.path) + .body(zone) + .end() + + expect(businessCreateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('updateZone', () => { + const zoneId = faker.string.uuid() + const zone: Omit = { label: faker.string.alpha({ length: 5 }), slug: faker.string.alpha({ length: 5, casing: 'lower' }), argocdUrl: faker.internet.url(), description: '' } + + it('should update and return zone for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce({ id: zoneId, ...zone }) + const response = await app.inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(1) + expect(response.json()).toEqual({ id: zoneId, ...zone }) + expect(response.statusCode).toEqual(200) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) + .body(zone) + .end() + + expect(businessUpdateMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) + + describe('deleteZone', () => { + it('should delete zone for admin', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(null) + const response = await app.inject() + .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(1) + expect(response.body).toBeFalsy() + expect(response.statusCode).toEqual(204) + }) + it('should pass business error', async () => { + const user = getUserMockInfos(true) + authUserMock.mockResolvedValueOnce(user) + + businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) + const response = await app.inject() + .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) + .end() + + expect(response.statusCode).toEqual(400) + }) + it('should return 403 for non-admin', async () => { + const user = getUserMockInfos(false) + authUserMock.mockResolvedValueOnce(user) + + const response = await app.inject() + .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) + .end() + + expect(businessDeleteMock).toHaveBeenCalledTimes(0) + expect(response.statusCode).toEqual(403) + }) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts index 7da2f17b1..b55c3fcd9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts @@ -1,86 +1,64 @@ -import { AdminAuthorized, zoneContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - Unauthorized401, -} from '@old-server/utils/errors'; +import { AdminAuthorized, zoneContract } from '@cpn-console/shared' +import { createZone, deleteZone, listZones, updateZone } from './business' +import { serverInstance } from '@old-server/app' -import { createZone, deleteZone, listZones, updateZone } from './business'; +import { authUser } from '@old-server/utils/controller' +import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' -@Injectable() -export class ZoneRouterService { - constructor(private readonly appService: AppService) {} +export function zoneRouter() { + return serverInstance.router(zoneContract, { + listZones: async () => { + const zones = await listZones() - zoneRouter() { - return this.appService.serverInstance.router(zoneContract, { - listZones: async () => { - const zones = await listZones(); + return { + status: 200, + body: zones, + } + }, - return { - status: 200, - body: zones, - }; - }, + createZone: async ({ request: req, body: data }) => { + const { user, adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user) return new Unauthorized401('Require to be requested from user not api key') - createZone: async ({ request: req, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); + const body = await createZone(data, user.id, req.id) + if (body instanceof ErrorResType) return body - const body = await createZone(data, user.id, req.id); - if (body instanceof ErrorResType) return body; + return { + status: 201, + body, + } + }, - return { - status: 201, - body, - }; - }, + updateZone: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user) return new Unauthorized401('Require to be requested from user not api key') - updateZone: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); + const zoneId = params.zoneId - const zoneId = params.zoneId; + const body = await updateZone(zoneId, data, user.id, req.id) + if (body instanceof ErrorResType) return body - const body = await updateZone(zoneId, data, user.id, req.id); - if (body instanceof ErrorResType) return body; + return { + status: 200, + body, + } + }, - return { - status: 200, - body, - }; - }, + deleteZone: async ({ request: req, params }) => { + const { user, adminPermissions } = await authUser(req) + if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + if (!user) return new Unauthorized401('Require to be requested from user not api key') + const zoneId = params.zoneId - deleteZone: async ({ request: req, params }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - const zoneId = params.zoneId; + const body = await deleteZone(zoneId, user.id, req.id) + if (body instanceof ErrorResType) return body - const body = await deleteZone(zoneId, user.id, req.id); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); - } + return { + status: 204, + body, + } + }, + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts index b73fdf72f..1ec57ceda 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts @@ -1,61 +1,57 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { exitGracefully, handleExit } from './server' +import { closeConnections } from './connect' +import { logger } from './app' -import { logger } from './app'; -import { closeConnections } from './connect'; -import { exitGracefully, handleExit } from './server'; +vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks')).mockSessionPlugin) +vi.mock('./init/db/index', () => ({ initDb: vi.fn() })) +vi.mock('./connect') -vi.mock( - 'fastify-keycloak-adapter', - (await import('./utils/mocks')).mockSessionPlugin, -); -vi.mock('./init/db/index', () => ({ initDb: vi.fn() })); -vi.mock('./connect'); - -process.exit = vi.fn(); +process.exit = vi.fn() vi.mock('./prepare-app', () => { - const app = { - listen: vi.fn(), - close: vi.fn(async () => {}), - }; - return { - getPreparedApp: () => Promise.resolve(app), - }; -}); -vi.spyOn(logger, 'info'); -vi.spyOn(logger, 'warn'); -vi.spyOn(logger, 'error'); -vi.spyOn(logger, 'fatal'); -vi.spyOn(logger, 'debug'); + const app = { + listen: vi.fn(), + close: vi.fn(async () => {}), + } + return { + getPreparedApp: () => Promise.resolve(app), + } +}) +vi.spyOn(logger, 'info') +vi.spyOn(logger, 'warn') +vi.spyOn(logger, 'error') +vi.spyOn(logger, 'fatal') +vi.spyOn(logger, 'debug') describe('server', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); + beforeEach(() => { + vi.clearAllMocks() + }) - it('should call closeConnections without parameter', async () => { - await exitGracefully(); + it('should call closeConnections without parameter', async () => { + await exitGracefully() - expect(closeConnections).toHaveBeenCalledTimes(1); - expect(closeConnections.mock.calls[0]).toHaveLength(0); - expect(logger.error).toHaveBeenCalledTimes(0); - }); + expect(closeConnections).toHaveBeenCalledTimes(1) + expect(closeConnections.mock.calls[0]).toHaveLength(0) + expect(logger.error).toHaveBeenCalledTimes(0) + }) - it('should log an error', async () => { - await exitGracefully(new Error('error')); + it('should log an error', async () => { + await exitGracefully(new Error('error')) - expect(closeConnections).toHaveBeenCalledTimes(1); - expect(closeConnections.mock.calls[0]).toHaveLength(0); - expect(logger.fatal).toHaveBeenCalledTimes(1); - expect(logger.fatal.mock.calls[0][0]).toBeInstanceOf(Error); - expect(logger.info).toHaveBeenCalledTimes(2); - }); + expect(closeConnections).toHaveBeenCalledTimes(1) + expect(closeConnections.mock.calls[0]).toHaveLength(0) + expect(logger.fatal).toHaveBeenCalledTimes(1) + expect(logger.fatal.mock.calls[0][0]).toBeInstanceOf(Error) + expect(logger.info).toHaveBeenCalledTimes(2) + }) - it('should call process.on 4 times', () => { - const processOn = vi.spyOn(process, 'on'); + it('should call process.on 4 times', () => { + const processOn = vi.spyOn(process, 'on') - handleExit(); + handleExit() - expect(processOn).toHaveBeenCalledTimes(5); - }); -}); + expect(processOn).toHaveBeenCalledTimes(5) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts index 62b6ad380..577540cac 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts @@ -1,159 +1,44 @@ -import { apiPrefix, getContract } from '@cpn-console/shared'; -import fastifyCookie from '@fastify/cookie'; -import helmet from '@fastify/helmet'; -import fastifySession from '@fastify/session'; -import fastifySwagger from '@fastify/swagger'; -import fastifySwaggerUi from '@fastify/swagger-ui'; -import { Injectable } from '@nestjs/common'; -import { initServer } from '@ts-rest/fastify'; -import { generateOpenApi } from '@ts-rest/open-api'; -import type { FastifyRequest } from 'fastify'; -import fastify from 'fastify'; -import keycloak from 'fastify-keycloak-adapter'; - -import { AppService } from './app'; -import { ConnectionService } from './connect'; -import { PrepareAppService } from './prepare-app'; -import { ResourcesService } from './resources/index'; -import { - isCI, - isDev, - isDevSetup, - isInt, - isProd, - isTest, - port, -} from './utils/env'; -import { FastifyService } from './utils/fastify'; -import { keycloakConf, sessionConf } from './utils/keycloak'; -import type { CustomLogger } from './utils/logger'; -import { LoggerService } from './utils/logger'; - -@Injectable() -export class ServerService { - constructor( - private readonly appService: AppService, - private readonly connectionService: ConnectionService, - private readonly fastifyService: FastifyService, - private readonly loggerService: LoggerService, - private readonly prepareAppService: PrepareAppService, - private readonly resourceService: ResourcesService, - ) {} - - app: any; - serverInstance: ReturnType = initServer(); - logger: any; - - handleExit() { - process.on('exit', this.logExitCode); - process.on('SIGINT', this.exitGracefully); - process.on('SIGTERM', this.exitGracefully); - process.on('uncaughtException', this.exitGracefully); - process.on('unhandledRejection', this.logUnhandledRejection); - } - - logExitCode(code: number) { - this.appService.logger.warn(`received signal: ${code}`); - } - - logUnhandledRejection(reason: unknown, promise: Promise) { - this.appService.logger.error({ - message: 'Unhandled Rejection', - promise, - reason, - }); - } - - async exitGracefully(error?: Error) { - if (error instanceof Error) { - this.appService.logger.fatal(error); - } - await this.app.close(); - this.appService.logger.info('Closing connections...'); - await this.connectionService.closeConnections(); - this.appService.logger.info('Exiting...'); - process.exit(error instanceof Error ? 1 : 0); - } - - async getApp(): Promise { - const app = await this.prepareAppService.getPreparedApp(); - - try { - await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }); - } catch (error) { - this.appService.logger.error(error); - process.exit(1); - } - - this.appService.logger.debug({ - isDev, - isTest, - isCI, - isDevSetup, - isProd, - }); - this.handleExit(); - } +import { getPreparedApp } from './prepare-app' +import { closeConnections } from './connect' +import { isCI, isDev, isDevSetup, isProd, isTest, port } from './utils/env' +import { logger } from './app' + +const app = await getPreparedApp() + +try { + await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }) +} catch (error) { + logger.error(error) + process.exit(1) +} - async createApp() { - const openApiDocument = generateOpenApi( - await getContract(), - this.fastifyService.swaggerConf, - { - setOperationId: true, - }, - ); +logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) + +export async function exitGracefully(error?: Error) { + if (error instanceof Error) { + logger.fatal(error) + } + await app.close() + logger.info('Closing connections...') + await closeConnections() + logger.info('Exiting...') + process.exit(error instanceof Error ? 1 : 0) +} - const app = fastify(this.fastifyService.fastifyConf) - .register(helmet, () => ({ - contentSecurityPolicy: !(isInt || isDev || isTest), - })) - .register(fastifyCookie) - .register(fastifySession, sessionConf) - // @ts-ignore - .register(keycloak, keycloakConf) - .register(fastifySwagger, { - transformObject: () => openApiDocument, - }) - .register(fastifySwaggerUi, this.fastifyService.swaggerConf as any) - .register(this.resourceService.apiRouter()) - .addHook('onRoute', (opts) => { - if (opts.path === `${apiPrefix}/healthz`) { - opts.logLevel = 'silent'; - } - }) - .setErrorHandler((error: Error, req: FastifyRequest, reply) => { - const statusCode = 500; - // @ts-ignore vérifier l'objet - const message = error.description || error.message; - reply.status(statusCode).send({ - status: statusCode, - error: message, - stack: error.stack, - }); - this.loggerService.log('info', { reqId: req.id, error }); - }) - .addHook('onResponse', (req, res) => { - if (res.statusCode < 400) { - req.log.info({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } else if (res.statusCode < 500) { - req.log.warn({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } else { - req.log.error({ - status: res.statusCode, - userId: req.session?.user?.id, - }); - } - }); +function logExitCode(code: number) { + logger.warn(`received signal: ${code}`) +} - await app.ready(); +function logUnhandledRejection(reason: unknown, promise: Promise) { + logger.error({ message: 'Unhandled Rejection', promise, reason }) +} - this.logger = app.log as CustomLogger; - } +export function handleExit() { + process.on('exit', logExitCode) + process.on('SIGINT', exitGracefully) + process.on('SIGTERM', exitGracefully) + process.on('uncaughtException', exitGracefully) + process.on('unhandledRejection', logUnhandledRejection) } + +handleExit() diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts index 4cc4d579c..1ffe757f3 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts @@ -1,46 +1,41 @@ -import { - type SharedSafeParseReturnType, - parseZodError, -} from '@cpn-console/shared'; +import { type SharedSafeParseReturnType, parseZodError } from '@cpn-console/shared' +import { BadRequest400 } from './errors' -import { BadRequest400 } from './errors'; - -export type Success = Result; -export type Failure = Result; +export type Success = Result +export type Failure = Result export class Result { - protected constructor( - readonly success: boolean, - readonly value: T | string, - ) {} - - static succeed(value: T): Success { - return new Result(true, value) as Success; - } - - static fail(message: string): Failure { - return new Result(false, message) as Failure; - } - - get isSuccess(): boolean { - return this.success; - } - - get isError(): boolean { - return !this.success; - } - - get data(): T { - if (this.success) return this.value as T; - throw new Error('Cannot get data from a Failure'); - } - - get error(): string { - if (!this.success) return this.value as string; - throw new Error('Cannot get error from a Success'); - } + protected constructor( + readonly success: boolean, + readonly value: T | string, + ) {} + + static succeed(value: T): Success { + return new Result(true, value) as Success + } + + static fail(message: string): Failure { + return new Result(false, message) as Failure + } + + get isSuccess(): boolean { + return this.success + } + + get isError(): boolean { + return !this.success + } + + get data(): T { + if (this.success) return this.value as T + throw new Error('Cannot get data from a Failure') + } + + get error(): string { + if (!this.success) return this.value as string + throw new Error('Cannot get error from a Success') + } } export function validateSchema(schemaValidation: SharedSafeParseReturnType) { - if (!schemaValidation.success) - return new BadRequest400(parseZodError(schemaValidation.error)); + if (!schemaValidation.success) return new BadRequest400(parseZodError(schemaValidation.error)) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts index 46a00ced3..7bf637c9e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts @@ -1,236 +1,169 @@ -import type { XOR } from '@cpn-console/shared'; -import { - PROJECT_PERMS as PP, - PROJECT_PERMS, - projectIsLockedInfo, - tokenHeaderName, -} from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; -import { - logViaSession, - logViaToken, -} from '@old-server/resources/user/business'; -import type { UserDetails } from '@old-server/types/index'; -import type { - Cluster, - Prisma, - Project, - ProjectMembers, - ProjectRole, -} from '@prisma/client'; -import type { FastifyRequest } from 'fastify'; - -import { Unauthorized401 } from './errors'; -import { uuid } from './queries-tools'; - -export type RequireOnlyOne = Pick< - T, - Exclude -> & - { - [K in Keys]-?: Required> & - Partial, undefined>>; - }[Keys]; - -type ErrorMessagePredicate = () => string | undefined; +import type { Cluster, Prisma, Project, ProjectMembers, ProjectRole } from '@prisma/client' +import type { XOR } from '@cpn-console/shared' +import { PROJECT_PERMS as PP, PROJECT_PERMS, projectIsLockedInfo, tokenHeaderName } from '@cpn-console/shared' +import type { FastifyRequest } from 'fastify' +import { Unauthorized401 } from './errors' +import { uuid } from './queries-tools' +import type { UserDetails } from '@old-server/types/index' +import prisma from '@old-server/prisma' +import { logViaSession, logViaToken } from '@old-server/resources/user/business' + +export type RequireOnlyOne = + Pick> + & { + [K in Keys]-?: + Required> + & Partial, undefined>> + }[Keys] + +type ErrorMessagePredicate = () => string | undefined export function getErrorMessage(...fns: ErrorMessagePredicate[]) { - for (const f of fns) { - const error = f(); - if (error) { - return error; - } + for (const f of fns) { + const error = f() + if (error) { + return error } + } } /** * Renvoie une erreur si le projet est verrouillé */ export function checkProjectLocked(project: { locked: boolean }): string { - return project.locked ? projectIsLockedInfo : ''; + return project.locked + ? projectIsLockedInfo + : '' } export function checkLocked(project: { locked: Project['locked'] }): string { - return checkProjectLocked(project); + return checkProjectLocked(project) } -export function checkClusterUnavailable( - clusterId: Cluster['id'], - authorizedClusterIds: Cluster['id'][], -): string { - return authorizedClusterIds.includes(clusterId) - ? '' - : "Ce cluster n'est pas disponible pour cette combinaison projet et stage"; +export function checkClusterUnavailable(clusterId: Cluster['id'], authorizedClusterIds: Cluster['id'][]): string { + return authorizedClusterIds.includes(clusterId) + ? '' + : 'Ce cluster n\'est pas disponible pour cette combinaison projet et stage' } -export const splitStringsFilterArray = >( - toMatch: T, - inputs: string, -): T => inputs.split(',').filter((i) => toMatch.includes(i)) as unknown as T; +export const splitStringsFilterArray = >(toMatch: T, inputs: string): T => inputs.split(',').filter(i => toMatch.includes(i)) as unknown as T -type StringArray = string[]; +type StringArray = string[] interface WhereBuilderParams { - enumValues: T; - eqValue: T[number] | undefined; - inValues: string | undefined; - notInValues: string | undefined; + enumValues: T + eqValue: T[number] | undefined + inValues: string | undefined + notInValues: string | undefined } -export function whereBuilder({ - enumValues, - eqValue, - inValues, - notInValues, -}: WhereBuilderParams) { - if (eqValue) { - return eqValue; - } else if (inValues) { - return { in: splitStringsFilterArray(enumValues, inValues) }; - } else if (notInValues) { - return { notIn: splitStringsFilterArray(enumValues, notInValues) }; - } +export function whereBuilder({ enumValues, eqValue, inValues, notInValues }: WhereBuilderParams) { + if (eqValue) { + return eqValue + } else if (inValues) { + return { in: splitStringsFilterArray(enumValues, inValues) } + } else if (notInValues) { + return { notIn: splitStringsFilterArray(enumValues, notInValues) } + } } -type ProjectMinimalPerms = Pick< - Project, - 'everyonePerms' | 'ownerId' | 'id' | 'locked' | 'status' -> & { roles: ProjectRole[]; members: ProjectMembers[] }; -export interface UserProfile { - user?: UserDetails; - adminPermissions: bigint; - tokenId?: string; -} -export interface ProjectPermState { - projectPermissions?: bigint; - projectId: Project['id']; - projectLocked: boolean; - projectStatus: Project['status']; - projectOwnerId: Project['ownerId']; -} -export type UserProjectProfile = UserProfile & ProjectPermState; +type ProjectMinimalPerms = Pick & { roles: ProjectRole[], members: ProjectMembers[] } +export interface UserProfile { user?: UserDetails, adminPermissions: bigint, tokenId?: string } +export interface ProjectPermState { projectPermissions?: bigint, projectId: Project['id'], projectLocked: boolean, projectStatus: Project['status'], projectOwnerId: Project['ownerId'] } +export type UserProjectProfile = UserProfile & ProjectPermState type ProjectUniqueFinder = XOR< - { slug: string }, - XOR< - { environmentId: string }, - XOR<{ repositoryId: string }, { id: string }> - > ->; - -const projectPermsSelect = { - roles: true, - members: true, - everyonePerms: true, - ownerId: true, - id: true, - locked: true, - status: true, -} as const satisfies Prisma.ProjectSelect; - -export async function authUser(req: FastifyRequest): Promise; -export async function authUser( - req: FastifyRequest, - projectUnique: ProjectUniqueFinder, -): Promise; -export async function authUser( - req: FastifyRequest, - projectUnique?: ProjectUniqueFinder, -): Promise { - let adminPermissions: bigint = 0n; - let tokenId: string | undefined; - let user: UserDetails | undefined; - - if (req.session.user) { - const loginResult = await logViaSession(req.session.user); - user = { - ...loginResult.user, - groups: req.session.user.groups, - }; - adminPermissions = loginResult.adminPerms; - } else { - const tokenHeader = req.headers[tokenHeaderName]; - if (typeof tokenHeader === 'string') { - const resultToken = await logViaToken(tokenHeader); - if (typeof resultToken === 'string') { - throw new Unauthorized401(resultToken); - } - adminPermissions = resultToken.adminPerms ?? 0n; - tokenId = resultToken.user.tokenId; - if (!user && resultToken.user) { - user = { ...resultToken.user, groups: [] }; - } - } - } - - const baseReturnInfos = { - user, - adminPermissions, - tokenId, - }; - if (!projectUnique || !user) { - return baseReturnInfos; - } - let project: ProjectMinimalPerms | null | undefined; - - if (projectUnique.repositoryId) { - project = ( - await prisma.repository.findUnique({ - where: { id: projectUnique.repositoryId }, - select: { project: { select: projectPermsSelect } }, - }) - )?.project; - } else if (projectUnique.environmentId) { - project = ( - await prisma.environment.findUnique({ - where: { id: projectUnique.environmentId }, - select: { project: { select: projectPermsSelect } }, - }) - )?.project; - } else if (projectUnique.id) { - project = uuid.test(projectUnique.id) - ? await prisma.project.findUnique({ - where: { id: projectUnique.id }, - select: projectPermsSelect, - }) - : await prisma.project.findUnique({ - where: { slug: projectUnique.id }, - select: projectPermsSelect, - }); - } else if (projectUnique.slug) { - project = await prisma.project.findFirstOrThrow({ - where: { slug: projectUnique.slug }, - select: projectPermsSelect, - }); + { slug: string }, + XOR<{ environmentId: string }, XOR<{ repositoryId: string }, { id: string }>> +> + +const projectPermsSelect = { roles: true, members: true, everyonePerms: true, ownerId: true, id: true, locked: true, status: true } as const satisfies Prisma.ProjectSelect + +export async function authUser(req: FastifyRequest): Promise +export async function authUser(req: FastifyRequest, projectUnique: ProjectUniqueFinder): Promise +export async function authUser(req: FastifyRequest, projectUnique?: ProjectUniqueFinder): Promise { + let adminPermissions: bigint = 0n + let tokenId: string | undefined + let user: UserDetails | undefined + + if (req.session.user) { + const loginResult = await logViaSession(req.session.user) + user = { + ...loginResult.user, + groups: req.session.user.groups, } - if (!project) { - return baseReturnInfos; + adminPermissions = loginResult.adminPerms + } else { + const tokenHeader = req.headers[tokenHeaderName] + if (typeof tokenHeader === 'string') { + const resultToken = await logViaToken(tokenHeader) + if (typeof resultToken === 'string') { + throw new Unauthorized401(resultToken) + } + adminPermissions = resultToken.adminPerms ?? 0n + tokenId = resultToken.user.tokenId + if (!user && resultToken.user) { + user = { ...resultToken.user, groups: [] } + } } - - const projectPermissions = getProjectPermissions(project, user); - - return { - user, - adminPermissions, - projectPermissions, - projectId: project.id, - projectLocked: project.locked, - projectStatus: project.status, - projectOwnerId: project.ownerId, - }; + } + + const baseReturnInfos = { + user, + adminPermissions, + tokenId, + } + if (!projectUnique || !user) { + return baseReturnInfos + } + let project: ProjectMinimalPerms | null | undefined + + if (projectUnique.repositoryId) { + project = (await prisma.repository.findUnique({ + where: { id: projectUnique.repositoryId }, + select: { project: { select: projectPermsSelect } }, + }))?.project + } else if (projectUnique.environmentId) { + project = (await prisma.environment.findUnique({ + where: { id: projectUnique.environmentId }, + select: { project: { select: projectPermsSelect } }, + }))?.project + } else if (projectUnique.id) { + project = uuid.test(projectUnique.id) + ? await prisma.project.findUnique({ + where: { id: projectUnique.id }, + select: projectPermsSelect, + }) + : await prisma.project.findUnique({ + where: { slug: projectUnique.id }, + select: projectPermsSelect, + }) + } else if (projectUnique.slug) { + project = await prisma.project.findFirstOrThrow({ + where: { slug: projectUnique.slug }, + select: projectPermsSelect, + }) + } + if (!project) { + return baseReturnInfos + } + + const projectPermissions = getProjectPermissions(project, user) + + return { + user, + adminPermissions, + projectPermissions, + projectId: project.id, + projectLocked: project.locked, + projectStatus: project.status, + projectOwnerId: project.ownerId, + } } -function getProjectPermissions( - project: ProjectMinimalPerms, - user: UserDetails, -): bigint | undefined { - if (project.ownerId === user.id) return PP.MANAGE; - const member = project.members.find((member) => member.userId === user.id); - if (!member) return; - - const memberRoles = project.roles.filter((role) => - member.roleIds.includes(role.id), - ); - return memberRoles.reduce( - (acc, curr) => acc | curr.permissions, - project.everyonePerms | PROJECT_PERMS.GUEST, - ); +function getProjectPermissions(project: ProjectMinimalPerms, user: UserDetails): bigint | undefined { + if (project.ownerId === user.id) return PP.MANAGE + const member = project.members.find(member => member.userId === user.id) + if (!member) return + + const memberRoles = project.roles.filter(role => member.roleIds.includes(role.id)) + return memberRoles.reduce((acc, curr) => acc | curr.permissions, project.everyonePerms | PROJECT_PERMS.GUEST) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts index 4e85b86c4..7cfd6c987 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts @@ -1,16 +1,15 @@ -import { describe, expect, it } from 'vitest'; - -import { getJSDateFromUtcIso } from './date'; +import { describe, expect, it } from 'vitest' +import { getJSDateFromUtcIso } from './date' describe('date-util', () => { - it('should return a native Date object', () => { - const date = '2022-10-11'; + it('should return a native Date object', () => { + const date = '2022-10-11' - const received = getJSDateFromUtcIso(date); + const received = getJSDateFromUtcIso(date) - expect(received.getMonth()).toBe(9); - expect(received.getFullYear()).toBe(2022); - expect(received.getDate()).toBeGreaterThan(10); - expect(received.getDate()).toBeLessThan(12); - }); -}); + expect(received.getMonth()).toBe(9) + expect(received.getFullYear()).toBe(2022) + expect(received.getDate()).toBeGreaterThan(10) + expect(received.getDate()).toBeLessThan(12) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts index 59e9c80b2..87473d262 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts @@ -1,5 +1,5 @@ -import { parseISO } from 'date-fns'; +import { parseISO } from 'date-fns' export function getJSDateFromUtcIso(dateUtcIso: string) { - return parseISO(dateUtcIso); + return parseISO(dateUtcIso) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts index f38754a1d..fc41aab75 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts @@ -1,61 +1,57 @@ -import * as dotenv from 'dotenv'; +import * as dotenv from 'dotenv' if (process.env.DOCKER !== 'true') { - dotenv.config({ path: '.env' }); + dotenv.config({ path: '.env' }) } if (process.env.INTEGRATION === 'true') { - const envInteg = dotenv.config({ path: '.env.integ' }); - process.env = { - ...process.env, - ...(envInteg?.parsed ?? {}), - }; + const envInteg = dotenv.config({ path: '.env.integ' }) + process.env = { + ...process.env, + ...(envInteg?.parsed ?? {}), + } } // application mode -export const isDev = process.env.NODE_ENV === 'development'; -export const isTest = process.env.NODE_ENV === 'test'; -export const isProd = process.env.NODE_ENV === 'production'; -export const isInt = process.env.INTEGRATION === 'true'; -export const isCI = process.env.CI === 'true'; -export const isDevSetup = process.env.DEV_SETUP === 'true'; +export const isDev = process.env.NODE_ENV === 'development' +export const isTest = process.env.NODE_ENV === 'test' +export const isProd = process.env.NODE_ENV === 'production' +export const isInt = process.env.INTEGRATION === 'true' +export const isCI = process.env.CI === 'true' +export const isDevSetup = process.env.DEV_SETUP === 'true' // app -export const port = process.env.SERVER_PORT; +export const port = process.env.SERVER_PORT export const appVersion = isProd - ? (process.env.APP_VERSION ?? 'unknown') - : 'dev'; + ? (process.env.APP_VERSION ?? 'unknown') + : 'dev' // db -export const dbUrl = process.env.DB_URL; +export const dbUrl = process.env.DB_URL // keycloak -export const sessionSecret = process.env.SESSION_SECRET; -export const keycloakProtocol = process.env.KEYCLOAK_PROTOCOL; -export const keycloakDomain = process.env.KEYCLOAK_DOMAIN; -export const keycloakRealm = process.env.KEYCLOAK_REALM; -export const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID; -export const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET; -export const keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI; +export const sessionSecret = process.env.SESSION_SECRET +export const keycloakProtocol = process.env.KEYCLOAK_PROTOCOL +export const keycloakDomain = process.env.KEYCLOAK_DOMAIN +export const keycloakRealm = process.env.KEYCLOAK_REALM +export const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID +export const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET +export const keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI export const adminsUserId = process.env.ADMIN_KC_USER_ID - ? process.env.ADMIN_KC_USER_ID.split(',') - : []; + ? process.env.ADMIN_KC_USER_ID.split(',') + : [] -export const contactEmail = - process.env.CONTACT_EMAIL ?? 'cloudpinative-relations@interieur.gouv.fr'; +export const contactEmail = process.env.CONTACT_EMAIL ?? 'cloudpinative-relations@interieur.gouv.fr' // plugins -export const mockPlugins = process.env.MOCK_PLUGINS === 'true'; -export const projectRootDir = process.env.PROJECTS_ROOT_DIR; -export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins'; -export const NODE_ENV = - process.env.NODE_ENV === 'test' - ? 'test' - : process.env.NODE_ENV === 'development' - ? 'development' - : 'production'; +export const mockPlugins = process.env.MOCK_PLUGINS === 'true' +export const projectRootDir = process.env.PROJECTS_ROOT_DIR +export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins' +export const NODE_ENV = process.env.NODE_ENV === 'test' + ? 'test' + : process.env.NODE_ENV === 'development' + ? 'development' + : 'production' // server tuning -export const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT - ? Number(process.env.PARALLEL_BULK_LIMIT) - : 5; +export const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT ? Number(process.env.PARALLEL_BULK_LIMIT) : 5 diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts index 4f8750b5e..0f1dd07fb 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts @@ -1,48 +1,48 @@ export class ErrorResType { - readonly status: 400 | 401 | 403 | 404 | 422 | 500; - body: { message: string } = { message: '' }; - constructor(code: 400 | 401 | 403 | 404 | 422 | 500) { - this.status = code; - } + readonly status: 400 | 401 | 403 | 404 | 422 | 500 + body: { message: string } = { message: '' } + constructor(code: 400 | 401 | 403 | 404 | 422 | 500) { + this.status = code + } } export class BadRequest400 extends ErrorResType { - constructor(message: string) { - super(400); - this.body.message = message ?? 'Bad Request'; - } + constructor(message: string) { + super(400) + this.body.message = message ?? 'Bad Request' + } } export class Unauthorized401 extends ErrorResType { - constructor(message?: string) { - super(401); - this.body.message = message ?? 'Unauthorized'; - } + constructor(message?: string) { + super(401) + this.body.message = message ?? 'Unauthorized' + } } export class Forbidden403 extends ErrorResType { - constructor(message?: string) { - super(403); - this.body.message = message ?? 'Forbidden'; - } + constructor(message?: string) { + super(403) + this.body.message = message ?? 'Forbidden' + } } export class NotFound404 extends ErrorResType { - constructor() { - super(404); - this.body.message = 'Not Found'; - } + constructor() { + super(404) + this.body.message = 'Not Found' + } } export class Unprocessable422 extends ErrorResType { - constructor(message?: string) { - super(422); - this.body.message = message ?? 'Unprocessable Entity'; - } + constructor(message?: string) { + super(422) + this.body.message = message ?? 'Unprocessable Entity' + } } export class Internal500 extends ErrorResType { - constructor(message: string) { - super(500); - this.body.message = message; - } + constructor(message: string) { + super(500) + this.body.message = message + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts index d10161411..ba161135f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts @@ -1,66 +1,55 @@ -import { swaggerUiPath } from '@cpn-console/shared'; -import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui'; -import { Injectable } from '@nestjs/common'; -import type { generateOpenApi } from '@ts-rest/open-api'; -import type { FastifyServerOptions } from 'fastify'; -import { randomUUID } from 'node:crypto'; - +import { randomUUID } from 'node:crypto' +import type { FastifyServerOptions } from 'fastify' +import type { generateOpenApi } from '@ts-rest/open-api' +import { swaggerUiPath } from '@cpn-console/shared' +import { loggerConf } from './logger' import { - NODE_ENV, - appVersion, - keycloakClientId, - keycloakClientSecret, - keycloakRealm, - keycloakRedirectUri, -} from './env'; -import { LoggerService } from './logger'; - -@Injectable() -export class FastifyService { - constructor(private readonly loggerService: LoggerService) { - this.fastifyConf = { - maxParamLength: 5000, - logger: - this.loggerService.loggerConf[NODE_ENV] ?? - this.loggerService.loggerConf.production, - genReqId: () => randomUUID(), - }; - } + NODE_ENV, + appVersion, + keycloakClientId, + keycloakClientSecret, + keycloakRealm, + keycloakRedirectUri, +} from './env' +import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui' - fastifyConf!: FastifyServerOptions; +export const fastifyConf: FastifyServerOptions = { + maxParamLength: 5000, + logger: loggerConf[NODE_ENV] ?? loggerConf.production, + genReqId: () => randomUUID(), +} - externalDocs = { - description: 'External documentation.', - url: 'https://cloud-pi-native.fr', - }; +const externalDocs = { + description: 'External documentation.', + url: 'https://cloud-pi-native.fr', +} - swaggerConf: Parameters[1] = { - info: { - title: 'Console Cloud Pi Native', - description: 'API de gestion des ressources Cloud Pi Native.', - version: appVersion, - }, +export const swaggerConf: Parameters[1] = { + info: { + title: 'Console Cloud Pi Native', + description: 'API de gestion des ressources Cloud Pi Native.', + version: appVersion, + }, - externalDocs: this.externalDocs, - servers: [ - { - url: keycloakRedirectUri, - }, - ], - }; + externalDocs, + servers: [ + { + url: keycloakRedirectUri, + }, + ], +} - swaggerUiConf: FastifySwaggerUiOptions = { - routePrefix: swaggerUiPath, - uiConfig: { - docExpansion: 'list', - deepLinking: false, - }, - initOAuth: { - clientId: keycloakClientId, - clientSecret: keycloakClientSecret, - realm: keycloakRealm, - appName: 'Cloud Pi Native', - scopes: 'openid generic', - }, - }; +export const swaggerUiConf: FastifySwaggerUiOptions = { + routePrefix: swaggerUiPath, + uiConfig: { + docExpansion: 'list', + deepLinking: false, + }, + initOAuth: { + clientId: keycloakClientId, + clientSecret: keycloakClientSecret, + realm: keycloakRealm, + appName: 'Cloud Pi Native', + scopes: 'openid generic', + }, } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts index 7d72a70fe..a31fffb0d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts @@ -1,265 +1,235 @@ -import type { - KubeCluster, - KubeUser, - Project as ProjectPayload, - Store, -} from '@cpn-console/hooks'; -import { describe, expect, it } from 'vitest'; - -import type { ProjectInfos, ReposCreds } from './hook-wrapper'; -import { transformToHookProject } from './hook-wrapper'; +import type { KubeCluster, KubeUser, Project as ProjectPayload, Store } from '@cpn-console/hooks' +import { describe, expect, it } from 'vitest' +import type { ProjectInfos, ReposCreds } from './hook-wrapper' +import { transformToHookProject } from './hook-wrapper' const associatedCluster = { - id: 'f0e39981-0b6d-4c16-aa96-225062b75767', - infos: '', - label: 'carno', - privacy: 'dedicated', - secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9b60', - kubeconfig: { - id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1437', - user: { - keyData: 'REDACTED', - certData: 'REDACTED', - }, - cluster: { - caData: 'REDACTED', - server: 'https://api-server:6443', - skipTLSVerify: false, - tlsServerName: 'api-server', - }, - createdAt: '2024-05-02T09:17:27.882Z', - updatedAt: '2024-05-02T09:17:27.882Z', + id: 'f0e39981-0b6d-4c16-aa96-225062b75767', + infos: '', + label: 'carno', + privacy: 'dedicated', + secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9b60', + kubeconfig: { + id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1437', + user: { + keyData: 'REDACTED', + certData: 'REDACTED', }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', - slug: 'default', + cluster: { + caData: 'REDACTED', + server: 'https://api-server:6443', + skipTLSVerify: false, + tlsServerName: 'api-server', }, -}; + createdAt: '2024-05-02T09:17:27.882Z', + updatedAt: '2024-05-02T09:17:27.882Z', + }, + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', + slug: 'default', + }, +} const nonAssociatedCluster = { - id: 'f0e39981-0b6d-4c16-aa96-225062b75111', - infos: '', - label: 'carno2', - privacy: 'dedicated', - secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9111', - kubeconfig: { - id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1111', - user: { - keyData: 'REDACTED', - certData: 'REDACTED', - }, - cluster: { - caData: 'REDACTED', - server: 'https://api-server:6443', - skipTLSVerify: false, - tlsServerName: 'api-server', - }, - createdAt: '2024-05-02T09:17:27.882Z', - updatedAt: '2024-05-02T09:17:27.882Z', + id: 'f0e39981-0b6d-4c16-aa96-225062b75111', + infos: '', + label: 'carno2', + privacy: 'dedicated', + secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9111', + kubeconfig: { + id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1111', + user: { + keyData: 'REDACTED', + certData: 'REDACTED', }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', - slug: 'default', + cluster: { + caData: 'REDACTED', + server: 'https://api-server:6443', + skipTLSVerify: false, + tlsServerName: 'api-server', }, -}; + createdAt: '2024-05-02T09:17:27.882Z', + updatedAt: '2024-05-02T09:17:27.882Z', + }, + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', + slug: 'default', + }, +} const project: ProjectInfos = { - id: '011e7860-04d7-461f-912d-334c622d38b3', - name: 'candilib', - description: "Application de réservation de places à l'examen du permis B.", - status: 'created', - locked: false, - createdAt: '2023-07-03T14:46:56.778Z', - updatedAt: '2023-07-03T14:46:56.783Z', - everyonePerms: 896n, - ownerId: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', - members: [], - clusters: [associatedCluster, nonAssociatedCluster], - environments: [ - { - id: '1b9f1053-fcf5-4053-a7b2-ff8a2c0c1921', - name: 'dev', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - createdAt: '2023-07-03T14:46:56.787Z', - updatedAt: '2023-07-03T14:46:56.803Z', - clusterId: 'aaaaaaaa-5b03-45d5-847b-149dec875680', - quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', - stageId: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', - quota: { - id: '5a57b62f-2465-4fb6-a853-5a751d099199', - memory: '4Gi', - cpu: 2, - name: 'small', - isPrivate: false, - }, - stage: { - id: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', - name: 'dev', - }, - cluster: { - id: 'aaaaaaaa-5b03-45d5-847b-149dec875680', - infos: 'Floating IP : 0.0.0.0', - label: 'pas-top-cluster', - privacy: 'dedicated', - secretName: '94d52618-7869-4192-b33e-85dd0959e815', - kubeconfig: { - id: 'b5662039-a62b-483e-ba54-b12c6f966c96', - user: { - token: 'kirikou', - }, - cluster: { - server: 'https://pwned.cluster', - tlsServerName: 'pwned.cluster', - }, - createdAt: '2024-07-24T16:54:14.969Z', - updatedAt: '2024-07-24T16:54:14.969Z', - }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', - slug: 'pub', - }, - }, + id: '011e7860-04d7-461f-912d-334c622d38b3', + name: 'candilib', + description: 'Application de réservation de places à l\'examen du permis B.', + status: 'created', + locked: false, + createdAt: '2023-07-03T14:46:56.778Z', + updatedAt: '2023-07-03T14:46:56.783Z', + everyonePerms: 896n, + ownerId: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', + members: [], + clusters: [associatedCluster, nonAssociatedCluster], + environments: [ + { + id: '1b9f1053-fcf5-4053-a7b2-ff8a2c0c1921', + name: 'dev', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + createdAt: '2023-07-03T14:46:56.787Z', + updatedAt: '2023-07-03T14:46:56.803Z', + clusterId: 'aaaaaaaa-5b03-45d5-847b-149dec875680', + quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', + stageId: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', + quota: { + id: '5a57b62f-2465-4fb6-a853-5a751d099199', + memory: '4Gi', + cpu: 2, + name: 'small', + isPrivate: false, + }, + stage: { + id: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', + name: 'dev', + }, + cluster: { + id: 'aaaaaaaa-5b03-45d5-847b-149dec875680', + infos: 'Floating IP : 0.0.0.0', + label: 'pas-top-cluster', + privacy: 'dedicated', + secretName: '94d52618-7869-4192-b33e-85dd0959e815', + kubeconfig: { + id: 'b5662039-a62b-483e-ba54-b12c6f966c96', + user: { + token: 'kirikou', + }, + cluster: { + server: 'https://pwned.cluster', + tlsServerName: 'pwned.cluster', + }, + createdAt: '2024-07-24T16:54:14.969Z', + updatedAt: '2024-07-24T16:54:14.969Z', }, - { - id: '1c654f00-4798-4a80-929f-960ddb37885a', - name: 'integration', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - createdAt: '2023-07-03T14:46:56.788Z', - updatedAt: '2023-07-03T14:46:56.803Z', - clusterId: '126ac57f-263c-4463-87bb-d4e9017056b2', - quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', - stageId: 'd434310e-7850-4d59-b47f-0772edf50582', - quota: { - id: '5a57b62f-2465-4fb6-a853-5a751d099199', - memory: '4Gi', - cpu: 2, - name: 'small', - isPrivate: false, - }, - stage: { - id: 'd434310e-7850-4d59-b47f-0772edf50582', - name: 'integration', - }, - cluster: { - id: '126ac57f-263c-4463-87bb-d4e9017056b2', - infos: null, - label: 'top-secret-cluster', - privacy: 'dedicated', - secretName: '59be2d50-58f9-42f3-95dc-b0c0518e3d8a', - kubeconfig: { - id: '0e88f000-07e6-4781-a69d-0963489387f7', - user: { - token: 'nyan cat', - }, - cluster: { - server: 'https://nothere.cluster', - skipTLSVerify: false, - tlsServerName: 'nothere.cluster', - }, - createdAt: '2024-07-24T16:54:14.966Z', - updatedAt: '2024-07-24T16:54:14.966Z', - }, - clusterResources: true, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', - slug: 'pub', - }, - }, + clusterResources: false, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', + slug: 'pub', }, - ], - repositories: [ - { - id: '299216bb-2dcc-42b5-ac71-6aa001d2dccf', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - internalRepoName: 'candilib', - externalRepoUrl: 'https://github.com/dnum-mi/candilib.git', - externalUserName: 'this-is-a-test', - isInfra: false, - isPrivate: true, - createdAt: '2023-07-03T14:46:56.788Z', - updatedAt: '2023-07-03T14:46:56.802Z', + }, + }, + { + id: '1c654f00-4798-4a80-929f-960ddb37885a', + name: 'integration', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + createdAt: '2023-07-03T14:46:56.788Z', + updatedAt: '2023-07-03T14:46:56.803Z', + clusterId: '126ac57f-263c-4463-87bb-d4e9017056b2', + quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', + stageId: 'd434310e-7850-4d59-b47f-0772edf50582', + quota: { + id: '5a57b62f-2465-4fb6-a853-5a751d099199', + memory: '4Gi', + cpu: 2, + name: 'small', + isPrivate: false, + }, + stage: { + id: 'd434310e-7850-4d59-b47f-0772edf50582', + name: 'integration', + }, + cluster: { + id: '126ac57f-263c-4463-87bb-d4e9017056b2', + infos: null, + label: 'top-secret-cluster', + privacy: 'dedicated', + secretName: '59be2d50-58f9-42f3-95dc-b0c0518e3d8a', + kubeconfig: { + id: '0e88f000-07e6-4781-a69d-0963489387f7', + user: { + token: 'nyan cat', + }, + cluster: { + server: 'https://nothere.cluster', + skipTLSVerify: false, + tlsServerName: 'nothere.cluster', + }, + createdAt: '2024-07-24T16:54:14.966Z', + updatedAt: '2024-07-24T16:54:14.966Z', + }, + clusterResources: true, + zone: { + id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', + slug: 'pub', }, - ], - plugins: [], - owner: { - id: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', - firstName: 'Jean', - lastName: 'DUPOND', - email: 'test@test.com', - createdAt: '2023-07-03T14:46:56.770Z', - updatedAt: '2023-07-03T14:46:56.770Z', - adminRoleIds: [], + }, }, - roles: [], -}; + ], + repositories: [ + { + id: '299216bb-2dcc-42b5-ac71-6aa001d2dccf', + projectId: '011e7860-04d7-461f-912d-334c622d38b3', + internalRepoName: 'candilib', + externalRepoUrl: 'https://github.com/dnum-mi/candilib.git', + externalUserName: 'this-is-a-test', + isInfra: false, + isPrivate: true, + createdAt: '2023-07-03T14:46:56.788Z', + updatedAt: '2023-07-03T14:46:56.802Z', + }, + ], + plugins: [], + owner: { + id: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', + firstName: 'Jean', + lastName: 'DUPOND', + email: 'test@test.com', + createdAt: '2023-07-03T14:46:56.770Z', + updatedAt: '2023-07-03T14:46:56.770Z', + adminRoleIds: [], + }, + roles: [], +} describe('transformToHookProject', () => { - // Mock data - const mockStore: Store = {}; - const mockReposCreds: ReposCreds = { - console: { - token: 'test', - username: 'test', - }, - }; + // Mock data + const mockStore: Store = {} + const mockReposCreds: ReposCreds = { + console: { + token: 'test', + username: 'test', + }, + } - it('transforme correctement le projet en objet Payload', () => { - const result: ProjectPayload = transformToHookProject( - project, - mockStore, - mockReposCreds, - ); + it('transforme correctement le projet en objet Payload', () => { + const result: ProjectPayload = transformToHookProject(project, mockStore, mockReposCreds) - // Asserts pour vérifier la transformation + // Asserts pour vérifier la transformation - // Assert sur la transformation des utilisateurs - expect(result.users).toEqual([project.owner]); + // Assert sur la transformation des utilisateurs + expect(result.users).toEqual([project.owner]) - // Assert sur la transformation des rôles - expect(result.roles).toEqual([ - { userId: project.owner.id, role: 'owner' }, - ]); + // Assert sur la transformation des rôles + expect(result.roles).toEqual([{ userId: project.owner.id, role: 'owner' }]) - // Assert sur la transformation des clusters - expect(result.clusters).toEqual( - [associatedCluster, nonAssociatedCluster].map( - ({ kubeconfig, ...cluster }) => ({ - user: kubeconfig.user as unknown as KubeUser, - cluster: kubeconfig.cluster as unknown as KubeCluster, - ...cluster, - privacy: cluster.privacy, - }), - ), - ); + // Assert sur la transformation des clusters + expect(result.clusters).toEqual([associatedCluster, nonAssociatedCluster].map(({ kubeconfig, ...cluster }) => ({ + user: kubeconfig.user as unknown as KubeUser, + cluster: kubeconfig.cluster as unknown as KubeCluster, + ...cluster, + privacy: cluster.privacy, + }))) - // Assert sur la transformation des environnements - expect(result.environments).toEqual( - project.environments.map( - ({ permissions: _, stage, quota, ...environment }) => ({ - quota, - stage: stage.name, - permissions: [ - { - permissions: { rw: true, ro: true }, - userId: project.ownerId, - }, - ], - ...environment, - apis: {}, - }), - ), - ); + // Assert sur la transformation des environnements + expect(result.environments).toEqual(project.environments.map(({ permissions: _, stage, quota, ...environment }) => ({ + quota, + stage: stage.name, + permissions: [{ permissions: { rw: true, ro: true }, userId: project.ownerId }], + ...environment, + apis: {}, + }))) - // Assert sur la transformation des repositories - expect(result.repositories).toEqual( - project.repositories.map((repo) => ({ - ...repo, - newCreds: mockReposCreds[repo.internalRepoName], - })), - ); + // Assert sur la transformation des repositories + expect(result.repositories).toEqual(project.repositories.map(repo => ({ ...repo, newCreds: mockReposCreds[repo.internalRepoName] }))) - // Assert sur le store - expect(result.store).toEqual(mockStore); - }); -}); + // Assert sur le store + expect(result.store).toEqual(mockStore) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts index f0d639def..1754a82d2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts @@ -1,371 +1,231 @@ -import type { - ClusterObject, - HookResult, - KubeCluster, - KubeUser, - Project as ProjectPayload, - RepoCreds, - Repository, - Store, - ZoneObject, -} from '@cpn-console/hooks'; -import { hooks } from '@cpn-console/hooks'; -import type { AsyncReturnType } from '@cpn-console/shared'; -import { - ProjectAuthorized, - getPermsByUserRoles, - resourceListToDict, -} from '@cpn-console/shared'; -import type { ConfigRecords } from '@old-server/resources/project-service/business'; -import { dbToObj } from '@old-server/resources/project-service/business'; -import { - archiveProject, - getAdminPlugin, - getClusterByIdOrThrow, - getClusterNamesByZoneId, - getClustersAssociatedWithProject, - getHookProjectInfos, - getHookRepository, - getProjectStore, - getZoneByIdOrThrow, - saveProjectStore, - updateProjectClusterHistory, - updateProjectCreated, - updateProjectFailed, - updateProjectWarning, -} from '@old-server/resources/queries-index'; -import type { - Cluster, - Kubeconfig, - Project, - ProjectRole, - Zone, -} from '@prisma/client'; - -import { genericProxy } from './proxy'; - -export type ReposCreds = Record; -export type ProjectInfos = AsyncReturnType; - -async function getProjectPayload( - projectId: Project['id'], - reposCreds?: ReposCreds, -) { - const [project, store, clusters] = await Promise.all([ - getHookProjectInfos(projectId), - getProjectStore(projectId), - getClustersAssociatedWithProject(projectId), - ]); - - return transformToHookProject( - { - ...project, - clusters, - }, - dbToObj(store), - reposCreds, - ); +import type { Cluster, Kubeconfig, Project, ProjectRole, Zone } from '@prisma/client' +import type { ClusterObject, HookResult, KubeCluster, KubeUser, Project as ProjectPayload, RepoCreds, Repository, Store, ZoneObject } from '@cpn-console/hooks' +import { hooks } from '@cpn-console/hooks' +import type { AsyncReturnType } from '@cpn-console/shared' +import { ProjectAuthorized, getPermsByUserRoles, resourceListToDict } from '@cpn-console/shared' +import { genericProxy } from './proxy' +import { archiveProject, getAdminPlugin, getClusterByIdOrThrow, getClusterNamesByZoneId, getClustersAssociatedWithProject, getHookProjectInfos, getHookRepository, getProjectStore, getZoneByIdOrThrow, saveProjectStore, updateProjectClusterHistory, updateProjectCreated, updateProjectFailed, updateProjectWarning } from '@old-server/resources/queries-index' +import type { ConfigRecords } from '@old-server/resources/project-service/business' +import { dbToObj } from '@old-server/resources/project-service/business' + +export type ReposCreds = Record +export type ProjectInfos = AsyncReturnType + +async function getProjectPayload(projectId: Project['id'], reposCreds?: ReposCreds) { + const [ + project, + store, + clusters, + ] = await Promise.all([ + getHookProjectInfos(projectId), + getProjectStore(projectId), + getClustersAssociatedWithProject(projectId), + ]) + + return transformToHookProject({ + ...project, + clusters, + }, dbToObj(store), reposCreds) } -async function upsertProject( - projectId: Project['id'], - reposCreds?: ReposCreds, -) { - const [payload, config] = await Promise.all([ - getProjectPayload(projectId, reposCreds), - getAdminPlugin(), - ]); - - const results = await hooks.upsertProject.execute(payload, dbToObj(config)); +async function upsertProject(projectId: Project['id'], reposCreds?: ReposCreds) { + const [payload, config] = await Promise.all([ + getProjectPayload(projectId, reposCreds), + getAdminPlugin(), + ]) - const records: ConfigRecords = Object.entries(results.results).reduce( - (acc, [pluginName, result]) => { - if (result.store) { - return [ - ...acc, - ...Object.entries(result.store).map(([key, value]) => ({ - pluginName, - key, - value: String(value), - })), - ]; - } - return acc; - }, - [] as ConfigRecords, - ); + const results = await hooks.upsertProject.execute(payload, dbToObj(config)) - await saveProjectStore(records, projectId); - const project = await manageProjectStatus( - projectId, - results, - 'upsert', - payload.environments.map((env) => env.clusterId), - ); - return { - results, - project, - }; + const records: ConfigRecords = Object.entries(results.results).reduce((acc, [pluginName, result]) => { + if (result.store) { + return [...acc, ...Object.entries(result.store).map(([key, value]) => ({ pluginName, key, value: String(value) }))] + } + return acc + }, [] as ConfigRecords) + + await saveProjectStore(records, projectId) + const project = await manageProjectStatus(projectId, results, 'upsert', payload.environments.map(env => env.clusterId)) + return { + results, + project, + } } const project = { - upsert: async (projectId: Project['id'], reposCreds?: ReposCreds) => { - const results = await upsertProject(projectId, reposCreds); - // automatically retry one time if it fails - return results.results.failed - ? upsertProject(projectId, reposCreds) - : results; - }, - delete: async (projectId: Project['id']) => { - const [payload, config] = await Promise.all([ - getProjectPayload(projectId), - getAdminPlugin(), - ]); - const results = await hooks.deleteProject.execute( - payload, - dbToObj(config), - ); - return { - results, - project: await manageProjectStatus( - projectId, - results, - 'delete', - [], - ), - }; - }, - getSecrets: async (projectId: Project['id']) => { - const project = await getHookProjectInfos(projectId); - const store = dbToObj(await getProjectStore(project.id)); - const config = dbToObj(await getAdminPlugin()); - - return hooks.getProjectSecrets.execute({ ...project, store }, config); - }, -} as const; - -type ProjectAction = keyof typeof project; -async function manageProjectStatus( - projectId: Project['id'], - hookReply: HookResult, - action: ProjectAction, - envClusterIds: Cluster['id'][], -): Promise> { - if (!hookReply.failed && hookReply.results?.kubernetes) { - await updateProjectClusterHistory(projectId, envClusterIds); - } - if (hookReply.failed) { - return updateProjectFailed(projectId); - } else if (hookReply.warning.length) { - return updateProjectWarning(projectId); - } else if (action === 'upsert') { - return updateProjectCreated(projectId); - } else if (action === 'delete') { - return archiveProject(projectId); + upsert: async (projectId: Project['id'], reposCreds?: ReposCreds) => { + const results = await upsertProject(projectId, reposCreds) + // automatically retry one time if it fails + return results.results.failed ? upsertProject(projectId, reposCreds) : results + }, + delete: async (projectId: Project['id']) => { + const [payload, config] = await Promise.all([ + getProjectPayload(projectId), + getAdminPlugin(), + ]) + const results = await hooks.deleteProject.execute(payload, dbToObj(config)) + return { + results, + project: await manageProjectStatus(projectId, results, 'delete', []), } - throw new Error('unknown action'); + }, + getSecrets: async (projectId: Project['id']) => { + const project = await getHookProjectInfos(projectId) + const store = dbToObj(await getProjectStore(project.id)) + const config = dbToObj(await getAdminPlugin()) + + return hooks.getProjectSecrets.execute({ ...project, store }, config) + }, +} as const + +type ProjectAction = keyof typeof project +async function manageProjectStatus(projectId: Project['id'], hookReply: HookResult, action: ProjectAction, envClusterIds: Cluster['id'][]): Promise> { + if (!hookReply.failed && hookReply.results?.kubernetes) { + await updateProjectClusterHistory(projectId, envClusterIds) + } + if (hookReply.failed) { + return updateProjectFailed(projectId) + } else if (hookReply.warning.length) { + return updateProjectWarning(projectId) + } else if (action === 'upsert') { + return updateProjectCreated(projectId) + } else if (action === 'delete') { + return archiveProject(projectId) + } + throw new Error('unknown action') } const cluster = { - upsert: async ( - clusterId: Cluster['id'], - previousZoneId: Cluster['zoneId'], - ) => { - const cluster = await getClusterByIdOrThrow(clusterId); - const clusterObject = cluster as unknown as ClusterObject; - const store = dbToObj(await getAdminPlugin()); - if (cluster.zoneId !== previousZoneId) { - // Upsert on the old zone to remove cluster - const previousClusterObject = { - ...cluster, - } as unknown as ClusterObject; - previousClusterObject.zone = - await getZoneByIdOrThrow(previousZoneId); - previousClusterObject.zone.clusterNames = - await getClusterNamesByZoneId(previousZoneId); - const hookResult = await hooks.upsertCluster.execute( - { - ...(cluster.kubeconfig as unknown as Pick< - ClusterObject, - 'cluster' | 'user' - >), - ...previousClusterObject, - }, - store, - ); - if (hookResult.failed) { - return hookResult; - } - } - clusterObject.zone.clusterNames = await getClusterNamesByZoneId( - cluster.zoneId, - ); - return hooks.upsertCluster.execute( - { - ...(cluster.kubeconfig as unknown as Pick< - ClusterObject, - 'cluster' | 'user' - >), - ...clusterObject, - }, - store, - ); - }, - delete: async (clusterId: Cluster['id']) => { - const cluster = await getClusterByIdOrThrow(clusterId); - const clusterObject = cluster as unknown as ClusterObject; - const clusterNames = await getClusterNamesByZoneId(cluster.zoneId); - clusterObject.zone.clusterNames = clusterNames.filter( - (c) => c !== cluster.label, - ); - const store = dbToObj(await getAdminPlugin()); - return hooks.deleteCluster.execute( - { - ...(cluster.kubeconfig as unknown as ClusterObject), - ...clusterObject, - }, - store, - ); - }, -} as const; + upsert: async (clusterId: Cluster['id'], previousZoneId: Cluster['zoneId']) => { + const cluster = await getClusterByIdOrThrow(clusterId) + const clusterObject = cluster as unknown as ClusterObject + const store = dbToObj(await getAdminPlugin()) + if (cluster.zoneId !== previousZoneId) { + // Upsert on the old zone to remove cluster + const previousClusterObject = { + ...cluster, + } as unknown as ClusterObject + previousClusterObject.zone = await getZoneByIdOrThrow(previousZoneId) + previousClusterObject.zone.clusterNames = await getClusterNamesByZoneId(previousZoneId) + const hookResult = await hooks.upsertCluster.execute({ + ...cluster.kubeconfig as unknown as Pick, + ...previousClusterObject, + }, store) + if (hookResult.failed) { + return hookResult + } + } + clusterObject.zone.clusterNames = await getClusterNamesByZoneId(cluster.zoneId) + return hooks.upsertCluster.execute({ + ...cluster.kubeconfig as unknown as Pick, + ...clusterObject, + }, store) + }, + delete: async (clusterId: Cluster['id']) => { + const cluster = await getClusterByIdOrThrow(clusterId) + const clusterObject = cluster as unknown as ClusterObject + const clusterNames = await getClusterNamesByZoneId(cluster.zoneId) + clusterObject.zone.clusterNames = clusterNames.filter(c => c !== cluster.label) + const store = dbToObj(await getAdminPlugin()) + return hooks.deleteCluster.execute({ + ...cluster.kubeconfig as unknown as ClusterObject, + ...clusterObject, + }, store) + }, +} as const const user = { - retrieveUserByEmail: async (email: string) => { - const config = dbToObj(await getAdminPlugin()); - return hooks.retrieveUserByEmail.execute({ email }, config); - }, -} as const; + retrieveUserByEmail: async (email: string) => { + const config = dbToObj(await getAdminPlugin()) + return hooks.retrieveUserByEmail.execute({ email }, config) + }, +} as const const zone = { - upsert: async (zoneId: Zone['id']) => { - const zone: ZoneObject = await getZoneByIdOrThrow(zoneId); - zone.clusterNames = await getClusterNamesByZoneId(zoneId); - const store = dbToObj(await getAdminPlugin()); - return hooks.upsertZone.execute(zone, store); - }, - delete: async (zoneId: Zone['id']) => { - const zone = await getZoneByIdOrThrow(zoneId); - const store = dbToObj(await getAdminPlugin()); - return hooks.deleteZone.execute(zone, store); - }, -} as const; + upsert: async (zoneId: Zone['id']) => { + const zone: ZoneObject = await getZoneByIdOrThrow(zoneId) + zone.clusterNames = await getClusterNamesByZoneId(zoneId) + const store = dbToObj(await getAdminPlugin()) + return hooks.upsertZone.execute(zone, store) + }, + delete: async (zoneId: Zone['id']) => { + const zone = await getZoneByIdOrThrow(zoneId) + const store = dbToObj(await getAdminPlugin()) + return hooks.deleteZone.execute(zone, store) + }, +} as const const misc = { - checkServices: async () => { - const config = dbToObj(await getAdminPlugin()); - return hooks.checkServices.execute({}, config); - }, - syncRepository: async ( - repoId: string, - { - syncAllBranches, - branchName, - }: { syncAllBranches: boolean; branchName?: string }, - ) => { - const { project, ...repoInfos } = await getHookRepository(repoId); - const store = dbToObj(await getProjectStore(project.id)); - const payload = { - repo: { ...repoInfos, syncAllBranches, branchName }, - ...project, - store, - }; - const config = dbToObj(await getAdminPlugin()); - return hooks.syncRepository.execute(payload, config); - }, -} as const; + checkServices: async () => { + const config = dbToObj(await getAdminPlugin()) + return hooks.checkServices.execute({}, config) + }, + syncRepository: async (repoId: string, { syncAllBranches, branchName }: { syncAllBranches: boolean, branchName?: string }) => { + const { project, ...repoInfos } = await getHookRepository(repoId) + const store = dbToObj(await getProjectStore(project.id)) + const payload = { + repo: { ...repoInfos, syncAllBranches, branchName }, + ...project, + store, + } + const config = dbToObj(await getAdminPlugin()) + return hooks.syncRepository.execute(payload, config) + }, +} as const export const hook = { - // @ts-ignore TODO voir comment opti la signature de la fonction - misc: genericProxy(misc), - // @ts-ignore TODO voir comment opti la signature de la fonction - project: genericProxy(project, { - upsert: ['delete'], - delete: ['upsert', 'delete'], - getSecrets: ['delete'], - }), - // @ts-ignore TODO voir comment opti la signature de la fonction - cluster: genericProxy(cluster, { - delete: ['upsert', 'delete'], - upsert: ['delete'], - }), - // @ts-ignore TODO voir comment opti la signature de la fonction - zone: genericProxy(zone, { delete: ['upsert'], upsert: ['delete'] }), - // @ts-ignore TODO voir comment opti la signature de la fonction - user: genericProxy(user, {}), -}; - -function formatClusterInfos({ - kubeconfig, - ...cluster -}: Omit & { - kubeconfig: Kubeconfig; - zone: Pick; -}) { - return { - user: kubeconfig.user as unknown as KubeUser, - cluster: kubeconfig.cluster as unknown as KubeCluster, - ...cluster, - privacy: cluster.privacy, - }; + // @ts-ignore TODO voir comment opti la signature de la fonction + misc: genericProxy(misc), + // @ts-ignore TODO voir comment opti la signature de la fonction + project: genericProxy(project, { upsert: ['delete'], delete: ['upsert', 'delete'], getSecrets: ['delete'] }), + // @ts-ignore TODO voir comment opti la signature de la fonction + cluster: genericProxy(cluster, { delete: ['upsert', 'delete'], upsert: ['delete'] }), + // @ts-ignore TODO voir comment opti la signature de la fonction + zone: genericProxy(zone, { delete: ['upsert'], upsert: ['delete'] }), + // @ts-ignore TODO voir comment opti la signature de la fonction + user: genericProxy(user, {}), } -export type RolesById = Record; - -export function transformToHookProject( - project: ProjectInfos, - store: Store, - reposCreds: ReposCreds = {}, -): ProjectPayload { - const clusters = project.clusters.map((cluster) => - formatClusterInfos(cluster), - ); - const rolesById = resourceListToDict(project.roles); - return { - ...project, - clusters, - environments: project.environments.map(({ stage, ...environment }) => ({ - stage: stage.name, - permissions: [ - { - permissions: { rw: true, ro: true }, - userId: project.ownerId, - }, - ...project.members.map((member) => ({ - userId: member.userId, - permissions: { - ro: ProjectAuthorized.ListEnvironments({ - adminPermissions: 0n, - projectPermissions: getPermsByUserRoles( - member.roleIds, - rolesById, - project.everyonePerms, - ), - }), - rw: ProjectAuthorized.ManageEnvironments({ - adminPermissions: 0n, - projectPermissions: getPermsByUserRoles( - member.roleIds, - rolesById, - project.everyonePerms, - ), - }), - }, - })), - ], - ...environment, - apis: {}, - })), - repositories: project.repositories.map((repo) => ({ - ...repo, - newCreds: reposCreds[repo.internalRepoName], +function formatClusterInfos({ kubeconfig, ...cluster }: Omit + & { kubeconfig: Kubeconfig, zone: Pick }) { + return { + user: kubeconfig.user as unknown as KubeUser, + cluster: kubeconfig.cluster as unknown as KubeCluster, + ...cluster, + privacy: cluster.privacy, + } +} +export type RolesById = Record + +export function transformToHookProject(project: ProjectInfos, store: Store, reposCreds: ReposCreds = {}): ProjectPayload { + const clusters = project.clusters.map(cluster => formatClusterInfos(cluster)) + const rolesById = resourceListToDict(project.roles) + + return ({ + ...project, + clusters, + environments: project.environments.map(({ stage, ...environment }) => ({ + stage: stage.name, + permissions: [ + { permissions: { rw: true, ro: true }, userId: project.ownerId }, + ...project.members.map(member => ({ + userId: member.userId, + permissions: { + ro: ProjectAuthorized.ListEnvironments({ adminPermissions: 0n, projectPermissions: getPermsByUserRoles(member.roleIds, rolesById, project.everyonePerms) }), + rw: ProjectAuthorized.ManageEnvironments({ adminPermissions: 0n, projectPermissions: getPermsByUserRoles(member.roleIds, rolesById, project.everyonePerms) }), + }, })), - store, - users: [project.owner, ...project.members.map(({ user }) => user)], - roles: [ - { userId: project.ownerId, role: 'owner' }, - ...project.members.map((member) => ({ - userId: member.userId, - role: 'user' as const, - })), - ], - }; + ], + ...environment, + apis: {}, + })), + repositories: project.repositories.map(repo => ({ ...repo, newCreds: reposCreds[repo.internalRepoName] })), + store, + users: [project.owner, ...project.members.map(({ user }) => user)], + roles: [ + { userId: project.ownerId, role: 'owner' }, + ...project.members.map(member => ({ + userId: member.userId, + role: 'user' as const, + })), + ], + }) } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts index 1675bb6be..5b7ce1b9c 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts @@ -1,46 +1,45 @@ -import { describe, expect, it } from 'vitest'; - -import { userPayloadMapper } from './keycloak-utils'; +import { describe, expect, it } from 'vitest' +import { userPayloadMapper } from './keycloak-utils' describe('keycloak', () => { - it('should map keycloak user object to DSO user object without groups', () => { - const payload = { - sub: 'thisIsAnId', - email: 'test@test.com', - given_name: 'Jean', - family_name: 'DUPOND', - }; - const desired = { - id: 'thisIsAnId', - email: 'test@test.com', - firstName: 'Jean', - lastName: 'DUPOND', - groups: [], - }; + it('should map keycloak user object to DSO user object without groups', () => { + const payload = { + sub: 'thisIsAnId', + email: 'test@test.com', + given_name: 'Jean', + family_name: 'DUPOND', + } + const desired = { + id: 'thisIsAnId', + email: 'test@test.com', + firstName: 'Jean', + lastName: 'DUPOND', + groups: [], + } - const transformed = userPayloadMapper(payload); + const transformed = userPayloadMapper(payload) - expect(transformed).toMatchObject(desired); - }); + expect(transformed).toMatchObject(desired) + }) - it('should map keycloak user object to DSO user object with groups', () => { - const payload = { - sub: 'thisIsAnId', - email: 'test@test.com', - given_name: 'Jean', - family_name: 'DUPOND', - groups: ['group1'], - }; - const desired = { - id: 'thisIsAnId', - email: 'test@test.com', - firstName: 'Jean', - lastName: 'DUPOND', - groups: ['group1'], - }; + it('should map keycloak user object to DSO user object with groups', () => { + const payload = { + sub: 'thisIsAnId', + email: 'test@test.com', + given_name: 'Jean', + family_name: 'DUPOND', + groups: ['group1'], + } + const desired = { + id: 'thisIsAnId', + email: 'test@test.com', + firstName: 'Jean', + lastName: 'DUPOND', + groups: ['group1'], + } - const transformed = userPayloadMapper(payload); + const transformed = userPayloadMapper(payload) - expect(transformed).toMatchObject(desired); - }); -}); + expect(transformed).toMatchObject(desired) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts index 870111176..462116029 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts @@ -1,27 +1,27 @@ -import { tokenHeaderName } from '@cpn-console/shared'; -import type { FastifyRequest } from 'fastify'; +import { tokenHeaderName } from '@cpn-console/shared' +import type { FastifyRequest } from 'fastify' interface KeycloakPayload { - sub: string; - email: string; - given_name: string; - family_name: string; - groups: string[]; + sub: string + email: string + given_name: string + family_name: string + groups: string[] } export function userPayloadMapper(userPayload: KeycloakPayload) { - return { - id: userPayload.sub, - email: userPayload.email, - firstName: userPayload.given_name, - lastName: userPayload.family_name, - groups: userPayload.groups || [], - }; + return { + id: userPayload.sub, + email: userPayload.email, + firstName: userPayload.given_name, + lastName: userPayload.family_name, + groups: userPayload.groups || [], + } } export function bypassFn(request: FastifyRequest) { - try { - return !!request.headers[tokenHeaderName]; - } catch (_e) {} - return false; + try { + return !!request.headers[tokenHeaderName] + } catch (_e) {} + return false } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts index 429b3f8bb..70905295e 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts @@ -1,47 +1,42 @@ +import { serviceContract, swaggerUiPath, systemContract } from '@cpn-console/shared' +import type { KeycloakOptions } from 'fastify-keycloak-adapter' import { - serviceContract, - swaggerUiPath, - systemContract, -} from '@cpn-console/shared'; -import type { KeycloakOptions } from 'fastify-keycloak-adapter'; - -import { - keycloakClientId, - keycloakClientSecret, - keycloakDomain, - keycloakProtocol, - keycloakRealm, - keycloakRedirectUri, - sessionSecret, -} from './env'; -import { bypassFn, userPayloadMapper } from './keycloak-utils'; + keycloakClientId, + keycloakClientSecret, + keycloakDomain, + keycloakProtocol, + keycloakRealm, + keycloakRedirectUri, + sessionSecret, +} from './env' +import { bypassFn, userPayloadMapper } from './keycloak-utils' export const keycloakConf = { - appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', - keycloakSubdomain: `${keycloakDomain}/realms/${keycloakRealm}`, - clientId: keycloakClientId ?? '', - clientSecret: keycloakClientSecret ?? '', - useHttps: keycloakProtocol === 'https', - disableCookiePlugin: true, - disableSessionPlugin: true, - // @ts-ignore - userPayloadMapper, - retries: 5, - excludedPatterns: [ - systemContract.getVersion.path, - systemContract.getHealth.path, - serviceContract.getServiceHealth.path, - `${swaggerUiPath}/**`, - ], - bypassFn, -} as const satisfies KeycloakOptions; + appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', + keycloakSubdomain: `${keycloakDomain}/realms/${keycloakRealm}`, + clientId: keycloakClientId ?? '', + clientSecret: keycloakClientSecret ?? '', + useHttps: keycloakProtocol === 'https', + disableCookiePlugin: true, + disableSessionPlugin: true, + // @ts-ignore + userPayloadMapper, + retries: 5, + excludedPatterns: [ + systemContract.getVersion.path, + systemContract.getHealth.path, + serviceContract.getServiceHealth.path, + `${swaggerUiPath}/**`, + ], + bypassFn, +} as const satisfies KeycloakOptions export const sessionConf = { - cookieName: 'sessionId', - secret: sessionSecret || 'a-very-strong-secret-with-more-than-32-char', - cookie: { - httpOnly: true, - secure: true, - }, - expires: 1_800_000, -}; + cookieName: 'sessionId', + secret: sessionSecret || 'a-very-strong-secret-with-more-than-32-char', + cookie: { + httpOnly: true, + secure: true, + }, + expires: 1_800_000, +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts index 86243f823..5e5ebd1b6 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts @@ -1,122 +1,97 @@ -import type { XOR } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import type { - FastifyBaseLogger, - FastifyLogFn, - PinoLoggerOptions, -} from 'fastify/types/logger'; +import type { FastifyBaseLogger, FastifyLogFn, PinoLoggerOptions } from 'fastify/types/logger' +import type { XOR } from '@cpn-console/shared' +import { logger as customLogger } from '@old-server/app' -type LoggerType = - | 'info' - | 'warn' - | 'error' - | 'fatal' - | 'trace' - | 'debug' - | 'audit' - | undefined; - -export interface CustomLogger extends FastifyBaseLogger { - /** - * Log at `'audit'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. - * If more args follows `msg`, these will be used to format `msg` using `util.format`. - * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string - */ - audit: FastifyLogFn; +export const customLevels = { + audit: 25, } -@Injectable() -export class LoggerService { - constructor(private readonly appService: AppService) {} - - customLevels = { - audit: 25, - }; - - loggerConf: Record = { - development: { - transport: { - target: 'pino-pretty', - options: { - translateTime: 'dd/mm/yyyy - HH:MM:ss Z', - ignore: 'pid,hostname', - colorize: true, - singleLine: true, - }, - }, - customLevels: this.customLevels, - level: process.env.LOG_LEVEL ?? 'debug', - }, - production: { - customLevels: this.customLevels, - level: process.env.LOG_LEVEL ?? 'audit', - }, - test: { - level: 'silent', - }, - }; +export const loggerConf: Record = { + development: { + transport: { + target: 'pino-pretty', + options: { + translateTime: 'dd/mm/yyyy - HH:MM:ss Z', + ignore: 'pid,hostname', + colorize: true, + singleLine: true, + }, + }, + customLevels, + level: process.env.LOG_LEVEL ?? 'debug', + }, + production: { + customLevels, + level: process.env.LOG_LEVEL ?? 'audit', + }, + test: { + level: 'silent', + }, +} - loggerWrapper = { - level: '', - child: () => this.loggerWrapper, - silent: () => {}, - audit: (msg: string | unknown) => console.log(msg), - info: (msg: string | unknown) => console.log(msg), - warn: (msg: string | unknown) => console.warn(msg), - error: (msg: string | unknown) => console.error(msg), - fatal: (msg: string | unknown) => console.error(msg), - trace: (msg: string | unknown) => console.trace(msg), - debug: (msg: string | unknown) => console.debug(msg), - }; +type LoggerType = 'info' | 'warn' | 'error' | 'fatal' | 'trace' | 'debug' | 'audit' | undefined +const loggerWrapper = { + level: '', + child: () => loggerWrapper, + silent: () => {}, + audit: (msg: string | unknown) => console.log(msg), + info: (msg: string | unknown) => console.log(msg), + warn: (msg: string | unknown) => console.warn(msg), + error: (msg: string | unknown) => console.error(msg), + fatal: (msg: string | unknown) => console.error(msg), + trace: (msg: string | unknown) => console.trace(msg), + debug: (msg: string | unknown) => console.debug(msg), +} - log( - type: LoggerType, - { - reqId, - userId, - tokenId, - message, - error, - infos, - }: { - reqId?: string; - userId?: string; - tokenId?: string; - infos?: Record; - } & XOR< - { message: string }, - { error: Record | string | Error } - >, - ) { - const logger = this.appService.logger || this.loggerWrapper; +export function log( + type: LoggerType, + { + reqId, + userId, + tokenId, + message, + error, + infos, + }: { + reqId?: string + userId?: string + tokenId?: string + infos?: Record + } & XOR<{ message: string }, { error: Record | string | Error }>, +) { + const logger = customLogger || loggerWrapper - const logInfos = { - message, - infos, - reqId, - userId, - tokenId, - }; + const logInfos = { + message, + infos, + reqId, + userId, + tokenId, + } - if (error) { - const errorInfos = { - ...logInfos, - error: { - message: - typeof error === 'string' - ? error - : error?.message || 'unexpected error', - trace: error instanceof Error && error?.stack, - }, - }; - logger.error({ ...errorInfos }); - return; - } - logger[type || 'info']({ reqId, userId, logInfos }); + if (error) { + const errorInfos = { + ...logInfos, + error: { + message: typeof error === 'string' ? error : error?.message || 'unexpected error', + trace: error instanceof Error && error?.stack, + }, } + logger.error({ ...errorInfos }) + return + } + logger[type || 'info']({ reqId, userId, logInfos }) +} + +export interface CustomLogger extends FastifyBaseLogger { + /** + * Log at `'audit'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. + * If more args follows `msg`, these will be used to format `msg` using `util.format`. + * + * @typeParam T: the interface of the object being serialized. Default is object. + * @param obj: object to be serialized + * @param msg: the log message to write + * @param ...args: format string values when `msg` is a format string + */ + audit: FastifyLogFn } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts index c784590ee..81eda4980 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts @@ -1,190 +1,152 @@ -import type { - PluginsManifests, - RepoCreds, - ServiceInfos, -} from '@cpn-console/hooks'; -import { editStrippers, populatePluginManifests } from '@cpn-console/hooks'; -import { DEFAULT, DISABLED, PROJECT_PERMS } from '@cpn-console/shared'; -import { faker } from '@faker-js/faker'; -import type { Repository } from '@prisma/client'; -import fp from 'fastify-plugin'; +import fp from 'fastify-plugin' +import type { Repository } from '@prisma/client' +import type { PluginsManifests, RepoCreds, ServiceInfos } from '@cpn-console/hooks' +import { editStrippers, populatePluginManifests } from '@cpn-console/hooks' +import { DEFAULT, DISABLED, PROJECT_PERMS } from '@cpn-console/shared' +import { faker } from '@faker-js/faker' +import type { UserDetails } from '../types/index' +import type * as utilsController from '../utils/controller' -import type { UserDetails } from '../types/index'; -import type * as utilsController from '../utils/controller'; - -let requestor: Requestor; +let requestor: Requestor export function setRequestor(user: Requestor = getRandomRequestor()) { - requestor = user; + requestor = user } export function getRequestor() { - return requestor; + return requestor } export async function mockSessionPlugin() { - const sessionPlugin = (app, opt, next) => { - app.addHook('onRequest', (req, res, next) => { - req.session = { user: getRequestor() }; - next(); - }); - next(); - }; + const sessionPlugin = (app, opt, next) => { + app.addHook('onRequest', (req, res, next) => { + req.session = { user: getRequestor() } + next() + }) + next() + } - return { default: fp(sessionPlugin) }; + return { default: fp(sessionPlugin) } } export async function mockHooksPackage() { - const hookTemplate = { - execute: () => ({ - args: {}, - failed: false, - }), - validate: () => ({ - failed: false, - }), - }; + const hookTemplate = { + execute: () => ({ + args: {}, + failed: false, + }), + validate: () => ({ + failed: false, + }), + } - return { - editStrippers, - populatePluginManifests, - services: { - getStatus: () => [], - refreshStatus: async () => [], - }, - PluginApi: class {}, - servicesInfos: { - registry: { title: 'Harbor', name: 'registry', to: () => 'test' }, - plugin2: { - title: 'Plugin2', - name: 'plugin2', - to: () => ({ to: 'test', title: 'Test' }), - }, - plugin3: { - title: 'Plugin3', - name: 'plugin3', - to: () => [{ to: 'test', title: 'Test' }], - }, - plugin4: { - title: 'Plugin4', - name: 'plugin4', - to: () => [{ to: 'test' }], - }, - plugin5: { title: 'Plugin5', name: 'plugin5' }, - } as Record, - pluginsManifests: { + return { + editStrippers, + populatePluginManifests, + services: { + getStatus: () => [], + refreshStatus: async () => [], + }, + PluginApi: class { }, + servicesInfos: { + registry: { title: 'Harbor', name: 'registry', to: () => 'test' }, + plugin2: { title: 'Plugin2', name: 'plugin2', to: () => ({ to: 'test', title: 'Test' }) }, + plugin3: { title: 'Plugin3', name: 'plugin3', to: () => [{ to: 'test', title: 'Test' }] }, + plugin4: { title: 'Plugin4', name: 'plugin4', to: () => [{ to: 'test' }] }, + plugin5: { title: 'Plugin5', name: 'plugin5' }, + } as Record, + pluginsManifests: { + registry: { + title: 'Harbor', + global: [{ + kind: 'switch', + initialValue: DEFAULT, + key: 'test2', + permissions: { + admin: { read: true, write: true }, + user: { read: true, write: false }, + }, + title: 'Test2', + value: DEFAULT, + description: 'description', + }], + project: [{ + kind: 'switch', + key: 'test2', + permissions: { + admin: { read: true, write: true }, + user: { read: true, write: true }, + }, + title: 'Test', + value: DEFAULT, + initialValue: DISABLED, + }], + }, + } as PluginsManifests, + hooks: { + // projects + getProjectSecrets: { + execute: () => ({ + failed: false, + args: {}, + results: { registry: { - title: 'Harbor', - global: [ - { - kind: 'switch', - initialValue: DEFAULT, - key: 'test2', - permissions: { - admin: { read: true, write: true }, - user: { read: true, write: false }, - }, - title: 'Test2', - value: DEFAULT, - description: 'description', - }, - ], - project: [ - { - kind: 'switch', - key: 'test2', - permissions: { - admin: { read: true, write: true }, - user: { read: true, write: true }, - }, - title: 'Test', - value: DEFAULT, - initialValue: DISABLED, - }, - ], - }, - } as PluginsManifests, - hooks: { - // projects - getProjectSecrets: { - execute: () => ({ - failed: false, - args: {}, - results: { - registry: { - secrets: { - token: 'myToken', - }, - status: { - failed: false, - }, - }, - }, - }), + secrets: { + token: 'myToken', + }, + status: { + failed: false, + }, }, - upsertProject: hookTemplate, - deleteProject: hookTemplate, - // clusters - upsertCluster: hookTemplate, - deleteCluster: hookTemplate, - // user - retrieveUserByEmail: hookTemplate, - }, - }; + }, + }), + }, + upsertProject: hookTemplate, + deleteProject: hookTemplate, + // clusters + upsertCluster: hookTemplate, + deleteCluster: hookTemplate, + // user + retrieveUserByEmail: hookTemplate, + }, + } } -export type ReposCreds = Record; +export type ReposCreds = Record -type Requestor = Partial; +type Requestor = Partial export function getRandomRequestor(user?: Requestor): Partial { - return { - id: user?.id ?? faker.string.uuid(), - email: user?.email ?? faker.internet.email(), - firstName: user?.firstName ?? faker.person.firstName(), - lastName: user?.lastName ?? faker.person.lastName(), - type: 'human', - ...(user?.groups !== null && { groups: user?.groups ?? [] }), - }; + return { + id: user?.id ?? faker.string.uuid(), + email: user?.email ?? faker.internet.email(), + firstName: user?.firstName ?? faker.person.firstName(), + lastName: user?.lastName ?? faker.person.lastName(), + type: 'human', + ...user?.groups !== null && { groups: user?.groups ?? [] }, + } } -export function getUserMockInfos( - isAdmin: boolean, - user?: UserDetails, -): utilsController.UserProfile & utilsController.ProjectPermState; -export function getUserMockInfos( - isAdmin: boolean, - user?: UserDetails, - project?: utilsController.ProjectPermState, -): utilsController.UserProjectProfile & utilsController.ProjectPermState; -export function getUserMockInfos( - isAdmin: boolean, - user = getRandomRequestor(), - project?: utilsController.ProjectPermState, -): utilsController.UserProfile | utilsController.UserProjectProfile { - return { - adminPermissions: isAdmin ? 2n : 0n, - user: user as unknown as UserDetails, - ...project, - }; +export function getUserMockInfos(isAdmin: boolean, user?: UserDetails): utilsController.UserProfile & utilsController.ProjectPermState +export function getUserMockInfos(isAdmin: boolean, user?: UserDetails, project?: utilsController.ProjectPermState): utilsController.UserProjectProfile & utilsController.ProjectPermState +export function getUserMockInfos(isAdmin: boolean, user = getRandomRequestor(), project?: utilsController.ProjectPermState): utilsController.UserProfile | utilsController.UserProjectProfile { + return { + adminPermissions: isAdmin ? 2n : 0n, + user, + ...project, + } } -export function getProjectMockInfos({ - projectId, - projectLocked, - projectOwnerId, - projectPermissions, - projectStatus, -}: Partial): utilsController.ProjectPermState { - return { - projectId: projectId ?? faker.string.uuid(), - projectLocked: projectLocked ?? false, - projectOwnerId: projectOwnerId ?? faker.string.uuid(), - projectStatus: projectStatus ?? 'created', - projectPermissions: projectPermissions ?? PROJECT_PERMS.MANAGE, - }; +export function getProjectMockInfos({ projectId, projectLocked, projectOwnerId, projectPermissions, projectStatus }: Partial): utilsController.ProjectPermState { + return { + projectId: projectId ?? faker.string.uuid(), + projectLocked: projectLocked ?? false, + projectOwnerId: projectOwnerId ?? faker.string.uuid(), + projectStatus: projectStatus ?? 'created', + projectPermissions: projectPermissions ?? PROJECT_PERMS.MANAGE, + } } export const atDates = { - createdAt: new Date(), - updatedAt: new Date(), -}; + createdAt: new Date(), + updatedAt: new Date(), +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts index 843158f6d..6fe145941 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts @@ -1,10 +1,9 @@ -import type { PluginManagerOptions } from '@cpn-console/hooks'; - -import { isCI, isInt, isProd } from './env'; +import type { PluginManagerOptions } from '@cpn-console/hooks' +import { isCI, isInt, isProd } from './env' export const pluginManagerOptions: PluginManagerOptions = { - mockHooks: isCI || (!isProd && !isInt), - mockMonitoring: isCI || (!isProd && !isInt), - mockExternalServices: isCI || (!isProd && !isInt), - startPlugins: (!isCI && isProd) || isInt, -}; + mockHooks: isCI || (!isProd && !isInt), + mockMonitoring: isCI || (!isProd && !isInt), + mockExternalServices: isCI || (!isProd && !isInt), + startPlugins: (!isCI && isProd) || isInt, +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts index 9ae100961..757246d06 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts @@ -1,163 +1,157 @@ -import { describe, expect, it } from 'vitest'; - -import { genericProxy } from './proxy'; +import { describe, expect, it } from 'vitest' +import { genericProxy } from './proxy' // Création d'une cible de test const target = { - async fetchData(id: string) { - return { id, data: 'Mocked data' }; - }, - async otherMethod(id: string) { - return { id, data: 'Mocked data' }; - }, -}; + async fetchData(id: string) { + return { id, data: 'Mocked data' } + }, + async otherMethod(id: string) { + return { id, data: 'Mocked data' } + }, +} describe('test calls without ID passed', () => { - // Test d'appel de méthode sans ID - it('calling method without ID', async () => { - const proxied = genericProxy(target); - const result = await proxied.fetchData(); - expect(result).toEqual({ id: undefined, data: 'Mocked data' }); - }); - - // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie - it('test when no ID is provided', async () => { - // Création d'une cible de test - const target = { - async fetchData() { - return 'No ID provided'; - }, - }; - - // Création du proxy - const proxied = genericProxy(target); - - // Appel à la méthode fetchData sans ID - const result = await proxied.fetchData(); - - // Vérification que le résultat est correct - expect(result).toBe('No ID provided'); - }); - - // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie avec une promesse en cours - it('test when no ID is provided with pending promise', async () => { - // Création d'une cible de test - const target = { - async fetchData() { - return new Promise((resolve) => - setTimeout(() => resolve('Pending result'), 100), - ); - }, - }; - - // Création du proxy - const proxied = genericProxy(target); - - // Appel à la méthode fetchData sans ID - const promise1 = proxied.fetchData(); - const promise2 = proxied.fetchData(); // Deuxième appel avant la résolution du premier - - // Attendre que la première promesse se résolve - const result1 = await promise1; - - // Vérification que le résultat de la première promesse est correct - expect(result1).toBe('Pending result'); - - // Attendre que la deuxième promesse se résolve - const result2 = await promise2; - - // Vérification que le résultat de la deuxième promesse est correct - expect(result2).toBe('Pending result'); - }); - // Test pour vérifier que l'erreur est levée lorsque args est fourni sans ID - it('test error when args provided without ID', async () => { - // Création d'une cible de test - const target = { - async fetchData(_id: string, _args: any) { - return 'No ID provided'; - }, - }; - - // Création du proxy - const proxied = genericProxy(target); - - const args = { key: 'value' }; - - // Appel de la fonction fetchData avec des arguments mais sans ID - await expect(proxied.fetchData(undefined, args)).rejects.toThrow( - 'ID is required when args are provided', - ); - }); -}); + // Test d'appel de méthode sans ID + it('calling method without ID', async () => { + const proxied = genericProxy(target) + const result = await proxied.fetchData() + expect(result).toEqual({ id: undefined, data: 'Mocked data' }) + }) + + // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie + it('test when no ID is provided', async () => { + // Création d'une cible de test + const target = { + async fetchData() { + return 'No ID provided' + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + // Appel à la méthode fetchData sans ID + const result = await proxied.fetchData() + + // Vérification que le résultat est correct + expect(result).toBe('No ID provided') + }) + + // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie avec une promesse en cours + it('test when no ID is provided with pending promise', async () => { + // Création d'une cible de test + const target = { + async fetchData() { + return new Promise(resolve => setTimeout(() => resolve('Pending result'), 100)) + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + // Appel à la méthode fetchData sans ID + const promise1 = proxied.fetchData() + const promise2 = proxied.fetchData() // Deuxième appel avant la résolution du premier + + // Attendre que la première promesse se résolve + const result1 = await promise1 + + // Vérification que le résultat de la première promesse est correct + expect(result1).toBe('Pending result') + + // Attendre que la deuxième promesse se résolve + const result2 = await promise2 + + // Vérification que le résultat de la deuxième promesse est correct + expect(result2).toBe('Pending result') + }) + // Test pour vérifier que l'erreur est levée lorsque args est fourni sans ID + it('test error when args provided without ID', async () => { + // Création d'une cible de test + const target = { + async fetchData(_id: string, _args: any) { + return 'No ID provided' + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + const args = { key: 'value' } + + // Appel de la fonction fetchData avec des arguments mais sans ID + await expect(proxied.fetchData(undefined, args)).rejects.toThrow('ID is required when args are provided') + }) +}) describe('test calls with ID passed', () => { - // Test d'appel de méthode avec ID - it('calling method with ID', async () => { - const proxied = genericProxy(target); - const result = await proxied.fetchData('123'); - expect(result).toEqual({ id: '123', data: 'Mocked data' }); - }); - - // Test d'appel de méthode avec exclusion en cours - it('calling method with exclusion in progress', async () => { - const proxied = genericProxy(target, { fetchData: ['otherMethod'] }); - // Simuler une exécution en cours pour la méthode exclue - proxied.otherMethod('456'); - - // Maintenant, tenter d'appeler fetchData pour le même ID devrait échouer - await expect(proxied.fetchData('456')).rejects.toThrow( - "otherMethod in progress on 456, can't fetchData", - ); - }); - - // Fonction de test asynchrone pour tester le mélange des nextArgs - it('test mixing nextArgs from concurrent promises', async () => { - // Création d'une cible de test - const target = { - async fetchData(id: string, args?: object) { - return { id, args }; - }, - }; - - // Création du proxy - const proxied = genericProxy(target); - - const promise1 = proxied.fetchData('123', { key1: 'value1' }); - // Appels successifs à fetchData avec différents arguments - const promise2 = proxied.fetchData('123', { key2: 'value2' }); - - // Promesse concurrente avec des nextArgs différents - const promise3 = proxied.fetchData('123', { key3: 'value3' }); - - // Attendre que les promesses se résolvent - const result1 = await promise1; - const result2 = await promise2; - const result3 = await promise3; - - // Vérification que les nextArgs de promise2 et promise3 ont été correctement mélangés - expect(result1.args).toEqual({ key1: 'value1' }); - expect(result2.args).toEqual({ key2: 'value2', key3: 'value3' }); - expect(result3.args).toEqual({ key2: 'value2', key3: 'value3' }); - }); - - it('test rejection of set attempt', () => { - // Création d'une cible de test - const target = { - async fetchData() { - return 'Mocked data'; - }, - }; - - // Création du proxy - const proxied = genericProxy(target); - - // Tentative de définir une nouvelle propriété sur le proxy - const setAttempt = () => { - proxied.fetchData = () => - new Promise((resolve) => resolve('illegal')); - }; - - // Vérification que la tentative de set est rejetée - expect(setAttempt).toThrow(TypeError); - }); -}); + // Test d'appel de méthode avec ID + it('calling method with ID', async () => { + const proxied = genericProxy(target) + const result = await proxied.fetchData('123') + expect(result).toEqual({ id: '123', data: 'Mocked data' }) + }) + + // Test d'appel de méthode avec exclusion en cours + it('calling method with exclusion in progress', async () => { + const proxied = genericProxy(target, { fetchData: ['otherMethod'] }) + // Simuler une exécution en cours pour la méthode exclue + proxied.otherMethod('456') + + // Maintenant, tenter d'appeler fetchData pour le même ID devrait échouer + await expect(proxied.fetchData('456')).rejects.toThrow( + 'otherMethod in progress on 456, can\'t fetchData', + ) + }) + + // Fonction de test asynchrone pour tester le mélange des nextArgs + it('test mixing nextArgs from concurrent promises', async () => { + // Création d'une cible de test + const target = { + async fetchData(id: string, args?: object) { + return { id, args } + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + const promise1 = proxied.fetchData('123', { key1: 'value1' }) + // Appels successifs à fetchData avec différents arguments + const promise2 = proxied.fetchData('123', { key2: 'value2' }) + + // Promesse concurrente avec des nextArgs différents + const promise3 = proxied.fetchData('123', { key3: 'value3' }) + + // Attendre que les promesses se résolvent + const result1 = await promise1 + const result2 = await promise2 + const result3 = await promise3 + + // Vérification que les nextArgs de promise2 et promise3 ont été correctement mélangés + expect(result1.args).toEqual({ key1: 'value1' }) + expect(result2.args).toEqual({ key2: 'value2', key3: 'value3' }) + expect(result3.args).toEqual({ key2: 'value2', key3: 'value3' }) + }) + + it('test rejection of set attempt', () => { + // Création d'une cible de test + const target = { + async fetchData() { + return 'Mocked data' + }, + } + + // Création du proxy + const proxied = genericProxy(target) + + // Tentative de définir une nouvelle propriété sur le proxy + const setAttempt = () => { + proxied.fetchData = () => new Promise(resolve => resolve('illegal')) + } + + // Vérification que la tentative de set est rejetée + expect(setAttempt).toThrow(TypeError) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts index f300e3a59..ef915a7d1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts @@ -1,99 +1,78 @@ // @ts-nocheck un enfer à typer, pour plus tard -type Tracker> = - | Record< - keyof T, - Record< - string, - { - currentExec?: Promise; - nextArgs?: [string]; - } - > - > - | Promise; +type Tracker> = Record + nextArgs?: [string] +}>> | Promise -type Target = Record Promise>; -type Excludes = - | Partial>> - | undefined; -const toTarget = (target: T) => ({ - tracker: {} as Tracker, - methods: target, -}); +type Target = Record Promise> +type Excludes = Partial>> | undefined +const toTarget = (target: T) => ({ tracker: {} as Tracker, methods: target }) // @ts-ignore export function genericProxy(proxied: T, excludes: Excludes = {}): T { - return new Proxy(toTarget(proxied), { - get({ methods, tracker }, property: string) { - if (!(property in methods)) return; - return async (...args) => { - const id = args[0] as string; + return new Proxy(toTarget(proxied), { + get({ methods, tracker }, property: string) { + if (!(property in methods)) return + return async (...args) => { + const id = args[0] as string - if (!id && args.length > 0) { - throw new Error('ID is required when args are provided'); - } + if (!id && args.length > 0) { + throw new Error('ID is required when args are provided') + } - if (!id) { - if (tracker[property] instanceof Promise) { - return tracker[property]; - } - const p = methods[property](); - if (p instanceof Promise) { - tracker[property] = p; - p.then(() => { - delete tracker[property]; - }); - } - return p; - } - if (!tracker[property]) { - tracker[property] = {}; - } + if (!id) { + if (tracker[property] instanceof Promise) { + return tracker[property] + } + const p = methods[property]() + if (p instanceof Promise) { + tracker[property] = p + p.then(() => { + delete tracker[property] + }) + } + return p + } + if (!tracker[property]) { + tracker[property] = {} + } - for (const testExclude of excludes[property] ?? []) { - // @ts-ignore - if (tracker?.[testExclude]?.[id]?.currentExec) { - throw new Error( - `${String(testExclude)} in progress on ${id}, can't ${String(property)}`, - ); - } - } + for (const testExclude of excludes[property] ?? []) { + // @ts-ignore + if (tracker?.[testExclude]?.[id]?.currentExec) { + throw new Error(`${String(testExclude)} in progress on ${id}, can't ${String(property)}`) + } + } - if (id in tracker[property]) { - if (args[1]) { - tracker[property][id].nextArgs = { - ...(tracker[property][id].nextArgs ?? {}), - ...args[1], - }; - } - if (tracker[property][id].currentExec) { - return new Promise((resolve) => { - tracker[property][id].currentExec.then(() => { - resolve( - tracker[property][id].currentExec ?? - methods[property]( - id, - tracker[property][id].nextArgs, - ), - ); - }); - }); - } - } - const p = methods[property](...args); - tracker[property][id] = { - currentExec: p, - nextArgs: undefined, - }; - tracker[property][id].currentExec = p; - p.then(() => { - tracker[property][id].currentExec = undefined; - }); - return p; - }; - }, - set() { - return false; - }, - }) as T; + if (id in tracker[property]) { + if (args[1]) { + tracker[property][id].nextArgs = { + ...(tracker[property][id].nextArgs ?? {}), + ...args[1], + } + } + if (tracker[property][id].currentExec) { + return new Promise((resolve) => { + tracker[property][id].currentExec.then(() => { + resolve(tracker[property][id].currentExec ?? methods[property](id, tracker[property][id].nextArgs)) + }) + }) + } + } + const p = methods[property](...args) + tracker[property][id] = { + currentExec: p, + nextArgs: undefined, + } + tracker[property][id].currentExec = p + p.then(() => { + tracker[property][id].currentExec = undefined + }) + return p + } + }, + set() { + return false + }, + }) as T } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts index c92489a9a..80f9740fc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts @@ -1,51 +1,47 @@ -import { exclude } from '@cpn-console/shared'; -import { describe, expect, it } from 'vitest'; - -import { filterObjectByKeys } from './queries-tools'; +import { describe, expect, it } from 'vitest' +import { exclude } from '@cpn-console/shared' +import { filterObjectByKeys } from './queries-tools' describe('queries-tools', () => { - it('should return a filtered object (filterObjectByKeys)', () => { - const initial = { - id: 'thisIsAnId', - name: 'alsoKeepThisKey', - description: 'keepThisKey', - }; - const desired = { - name: 'alsoKeepThisKey', - description: 'keepThisKey', - }; + it('should return a filtered object (filterObjectByKeys)', () => { + const initial = { + id: 'thisIsAnId', + name: 'alsoKeepThisKey', + description: 'keepThisKey', + } + const desired = { + name: 'alsoKeepThisKey', + description: 'keepThisKey', + } - const transformed = filterObjectByKeys(initial, [ - 'name', - 'description', - ]); + const transformed = filterObjectByKeys(initial, ['name', 'description']) - expect(transformed).toMatchObject(desired); - }); + expect(transformed).toMatchObject(desired) + }) - it('should return a filtered object (exclude)', () => { - const initial = { - id: 'thisIsAnId', - name: 'myProjectName', - environment: { - permissions: { - password: 'secret', - id: 'notSecret', - }, - }, - }; - const desired = { - id: 'thisIsAnId', - name: 'myProjectName', - environment: { - permissions: { - id: 'notSecret', - }, - }, - }; + it('should return a filtered object (exclude)', () => { + const initial = { + id: 'thisIsAnId', + name: 'myProjectName', + environment: { + permissions: { + password: 'secret', + id: 'notSecret', + }, + }, + } + const desired = { + id: 'thisIsAnId', + name: 'myProjectName', + environment: { + permissions: { + id: 'notSecret', + }, + }, + } - const transformed = exclude(initial, ['password']); + const transformed = exclude(initial, ['password']) - expect(transformed).toMatchObject(desired); - }); -}); + expect(transformed).toMatchObject(desired) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts index fb9fc0bb6..856ca277f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts @@ -1,12 +1,11 @@ -export const dbKeysExcluded = ['updatedAt', 'createdAt']; +export const dbKeysExcluded = ['updatedAt', 'createdAt'] // TODO // @ts-ignore supprimer cette fonction et utiliser des schémas zod où elle est utilisé export function filterObjectByKeys(obj, keys) { - return Object.fromEntries( - Object.entries(obj)?.filter(([key, _value]) => keys.includes(key)), - ); + return Object.fromEntries( + Object.entries(obj)?.filter(([key, _value]) => keys.includes(key)), + ) } -export const uuid: RegExp = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +export const uuid: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts index 3706edf64..f754da343 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts @@ -1,154 +1,148 @@ -import { createRandomDbSetup } from '@cpn-console/test-utils'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest' +import { createRandomDbSetup } from '@cpn-console/test-utils' describe('random utils', () => { - // TODO - it.skip('should create a random db for tests', () => { - const db = createRandomDbSetup({ - nbUsers: 3, - nbRepo: 1, - envs: ['dev', 'prod'], - }); - expect(db).toEqual( - expect.objectContaining({ - stages: expect.arrayContaining([ - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - ]), - quotas: expect.arrayContaining([ - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - ]), - project: expect.objectContaining({ - id: expect.any(String), - name: expect.any(String), - clusters: expect.arrayContaining([ - { - caData: expect.any(String), - server: expect.any(String), - tlsServername: expect.any(String), - }, - ]), - status: expect.any(String), - locked: expect.any(Boolean), - roles: expect.arrayContaining([ - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - ]), - repositories: expect.any(Array), - environments: expect.arrayContaining([ - { - id: expect.any(String), - stageId: expect.any(String), - projectId: expect.any(String), - quotaId: expect.any(String), - status: expect.any(String), - permissions: expect.any(Array), - clusters: expect.any(Array), - }, - { - id: expect.any(String), - stageId: expect.any(String), - projectId: expect.any(String), - quotaId: expect.any(String), - status: expect.any(String), - permissions: expect.any(Array), - clusters: expect.any(Array), - }, - ]), - }), - users: expect.arrayContaining([ - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - ]), - }), - ); - }); -}); + // TODO + it.skip('should create a random db for tests', () => { + const db = createRandomDbSetup({ nbUsers: 3, nbRepo: 1, envs: ['dev', 'prod'] }) + expect(db).toEqual( + expect.objectContaining({ + stages: expect.arrayContaining([ + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + { + id: expect.any(String), + name: expect.any(String), + }, + ]), + quotas: expect.arrayContaining([ + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + { + id: expect.any(String), + name: expect.any(String), + memory: expect.any(String), + cpu: expect.any(Number), + isPrivate: expect.any(Boolean), + }, + ]), + project: expect.objectContaining({ + id: expect.any(String), + name: expect.any(String), + clusters: expect.arrayContaining([{ + caData: expect.any(String), + server: expect.any(String), + tlsServername: expect.any(String), + }]), + status: expect.any(String), + locked: expect.any(Boolean), + roles: expect.arrayContaining([ + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + { + userId: expect.any(String), + projectId: expect.any(String), + role: expect.any(String), + user: expect.objectContaining({ + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }), + }, + ]), + repositories: expect.any(Array), + environments: expect.arrayContaining([ + { + id: expect.any(String), + stageId: expect.any(String), + projectId: expect.any(String), + quotaId: expect.any(String), + status: expect.any(String), + permissions: expect.any(Array), + clusters: expect.any(Array), + }, + { + id: expect.any(String), + stageId: expect.any(String), + projectId: expect.any(String), + quotaId: expect.any(String), + status: expect.any(String), + permissions: expect.any(Array), + clusters: expect.any(Array), + }, + ]), + }), + users: expect.arrayContaining([ + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + { + id: expect.any(String), + email: expect.any(String), + firstName: expect.any(String), + lastName: expect.any(String), + }, + ]), + }), + ) + }) +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts index 68cb46a9a..12ad9d735 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts @@ -1,15 +1,18 @@ /// -import path from 'path'; -import { defineConfig } from 'vite'; +import { URL, fileURLToPath } from 'node:url' +import { defineConfig } from 'vite' export default defineConfig({ - plugins: [], - resolve: { - alias: { - '@': path.join(__dirname, '..', '/src'), - }, + plugins: [ + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), }, - test: { - poolMatchGlobs: [['**/resources/**/*.spec', 'forks']], - }, -} as any); + }, + test: { + poolMatchGlobs: [ + ['**/resources/**/*.spec', 'forks'], + ], + }, +}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts index 6dd4100e5..596420f7d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts @@ -1,11 +1,11 @@ -process.env.ARGOCD_URL = 'https://argo-cd.readthedocs.io'; -process.env.GITLAB_URL = 'https://gitlab.com'; -process.env.HARBOR_URL = 'https://goharbor.io'; -process.env.NEXUS_URL = 'https://sonatype.com/products/nexus-repository'; -process.env.SONARQUBE_URL = 'https://www.sonarqube.org'; -process.env.VAULT_URL = 'https://www.vaultproject.io'; -process.env.PROJECTS_ROOT_DIR = 'forge-mi/projects'; -process.env.KEYCLOAK_REDIRECT_URI = 'http://console.dso.local'; -process.env.CONTACT_EMAIL = 'cloudpinative-relations@interieur.gouv.fr'; -process.env.OPENCDS_URL = 'https://opencds.gouv.fr'; -process.env.OPENCDS_API_TOKEN = 'test_token'; +process.env.ARGOCD_URL = 'https://argo-cd.readthedocs.io' +process.env.GITLAB_URL = 'https://gitlab.com' +process.env.HARBOR_URL = 'https://goharbor.io' +process.env.NEXUS_URL = 'https://sonatype.com/products/nexus-repository' +process.env.SONARQUBE_URL = 'https://www.sonarqube.org' +process.env.VAULT_URL = 'https://www.vaultproject.io' +process.env.PROJECTS_ROOT_DIR = 'forge-mi/projects' +process.env.KEYCLOAK_REDIRECT_URI = 'http://console.dso.local' +process.env.CONTACT_EMAIL = 'cloudpinative-relations@interieur.gouv.fr' +process.env.OPENCDS_URL = 'https://opencds.gouv.fr' +process.env.OPENCDS_API_TOKEN = 'test_token' diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts index 8d234533c..c8d5ceaa7 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts @@ -1,34 +1,34 @@ -import { mergeConfig } from 'vite'; -import { configDefaults, defineConfig } from 'vitest/config'; - -import viteConfig from './vite.config'; +import { fileURLToPath } from 'node:url' +import { mergeConfig } from 'vite' +import { configDefaults, defineConfig } from 'vitest/config' +import viteConfig from './vite.config' export default mergeConfig( - viteConfig as any, - defineConfig({ - test: { - reporters: ['default', 'hanging-process'], - environment: 'node', - testTimeout: 2000, - coverage: { - provider: 'v8', - reporter: ['text', 'lcov'], - include: ['src/**'], - exclude: [ - '**/types', - '**/mocks', - '**/*.spec', - '**/*.d', - '**/*.vue', - '**/queries', - '**/mocks', - ], - }, - include: ['src/**/*.spec.{ts,js}'], - exclude: [...configDefaults.exclude, 'e2e/*'], - setupFiles: ['./vitest-init'], - root: __dirname, - pool: 'forks', - }, - }), -); + viteConfig, + defineConfig({ + test: { + reporters: ['default', 'hanging-process'], + environment: 'node', + testTimeout: 2000, + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**'], + exclude: [ + '**/types', + '**/mocks', + '**/*.spec', + '**/*.d', + '**/*.vue', + '**/queries', + '**/mocks', + ], + }, + include: ['src/**/*.spec.{ts,js}'], + exclude: [...configDefaults.exclude, 'e2e/*'], + setupFiles: ['./vitest-init'], + root: fileURLToPath(new URL('./', import.meta.url)), + pool: 'forks', + }, + }), +) From f4340b611a4bfc63132b470d0fb39e42cd4a2f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Fri, 12 Dec 2025 15:47:43 +0100 Subject: [PATCH 18/33] chore(nest-js): add basic config and logger modules based on existing implementation --- apps/server-nestjs/README.md | 15 ++++++ apps/server-nestjs/package.json | 2 + .../src/cpin-module/cpin.module.ts | 18 +------ .../configuration/configuration.module.ts | 9 ++++ .../configuration/configuration.service.ts | 5 +- .../infrastructure/infrastructure.module.ts | 10 ++-- .../infrastructure/logger/logger.module.ts | 50 +++++++++++++++++++ .../logger/logger.service.spec.ts | 18 ------- .../infrastructure/logger/logger.service.ts | 4 -- apps/server-nestjs/src/main.ts | 4 +- pnpm-lock.yaml | 50 +++++++++++++++++++ 11 files changed, 141 insertions(+), 44 deletions(-) create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.module.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts delete mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.ts diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md index 6d1a8b473..bc9c72411 100644 --- a/apps/server-nestjs/README.md +++ b/apps/server-nestjs/README.md @@ -227,3 +227,18 @@ flowchart TD Kubernetes --> LoggerService Dots --> LoggerService ``` + +To update `old-server` (after rebasing on `origin/master`, for instance) : + +```bash +server-nestjs/$ rm -rf src/cpin-module/old-server +server-nestjs/$ cp -r ../server src/cpin-module/old-server +server-nestjs/$ find src/cpin-module/old-server -type f -iname "*.ts" -exec sed -i -e "s#@/#@old-server/#g" {} \; +server-nestjs/$ find src/cpin-module/old-server -type f -iname "*.ts" -exec sed -i -e "s#\.[jt]s'#'#g" {} \; +``` + +## To delete (once we have a sastifying nestjs implementation): + +``` +old-server/src/utils/logger.ts +``` diff --git a/apps/server-nestjs/package.json b/apps/server-nestjs/package.json index de827ab2b..146176fa5 100644 --- a/apps/server-nestjs/package.json +++ b/apps/server-nestjs/package.json @@ -48,6 +48,8 @@ "fastify-keycloak-adapter": "2.3.2", "json-2-csv": "^5.5.7", "mustache": "^4.2.0", + "nestjs-pino": "^4.5.0", + "pino-http": "^11.0.0", "prisma": "^6.0.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/apps/server-nestjs/src/cpin-module/cpin.module.ts b/apps/server-nestjs/src/cpin-module/cpin.module.ts index 15a7406c1..cced22024 100644 --- a/apps/server-nestjs/src/cpin-module/cpin.module.ts +++ b/apps/server-nestjs/src/cpin-module/cpin.module.ts @@ -1,11 +1,5 @@ import { Module } from '@nestjs/common'; -import { AppService } from '@old-server/app'; -import { ConnectionService } from '@old-server/connect'; -import { PrepareAppService } from '@old-server/prepare-app'; -import { ResourcesService } from '@old-server/resources'; -import { ServerService } from '@old-server/server'; -import { FastifyService } from '@old-server/utils/fastify'; -import { CustomLoggerService } from '@old-server/utils/logger'; + import { ApplicationInitializationModule } from './application-initialization/application-initialization.module'; import { InfrastructureModule } from './infrastructure/infrastructure.module'; @@ -14,15 +8,7 @@ import { InfrastructureModule } from './infrastructure/infrastructure.module'; // as many modules as possible ! @Module({ controllers: [], - providers: [ - AppService, - ConnectionService, - FastifyService, - CustomLoggerService, - PrepareAppService, - ResourcesService, - ServerService, - ], + providers: [], imports: [ApplicationInitializationModule, InfrastructureModule], }) export class CpinModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.module.ts new file mode 100644 index 000000000..c68567a33 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { ConfigurationService } from './configuration.service'; + +@Module({ + providers: [ConfigurationService], + exports: [ConfigurationService] +}) +export class ConfigurationModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts index f2d3dae67..b0a1e4f9b 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts @@ -1,4 +1,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() -export class ConfigurationService {} +export class ConfigurationService { + // @TODO: Rework this with proper environment handling + public readonly environment = 'development'; +} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts index 7657aa206..e4eb179f4 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; -import { LoggerService } from './logger/logger.service'; + +import { ConfigurationModule } from './configuration/configuration.module'; import { DatabaseService } from './database/database.service'; -import { HttpClientService } from './http-client/http-client.service'; import { FastifyService } from './fastify/fastify.service'; -import { ConfigurationService } from './configuration/configuration.service'; +import { HttpClientService } from './http-client/http-client.service'; +import { LoggerModule } from './logger/logger.module'; @Module({ - providers: [LoggerService, DatabaseService, HttpClientService, FastifyService, ConfigurationService] + providers: [DatabaseService, HttpClientService, FastifyService], + imports: [LoggerModule, ConfigurationModule], }) export class InfrastructureModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts new file mode 100644 index 000000000..1c0ee6a4d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts @@ -0,0 +1,50 @@ +import { Module } from '@nestjs/common'; +import { PinoLoggerOptions } from 'fastify/types/logger'; +import { LoggerModule as PinoLoggerModule } from 'nestjs-pino'; + +import { ConfigurationModule } from '../configuration/configuration.module'; +import { ConfigurationService } from '../configuration/configuration.service'; + +export const customLevels = { + audit: 25, +}; + +export const loggerConfiguration: Record = { + development: { + transport: { + target: 'pino-pretty', + options: { + translateTime: 'dd/mm/yyyy - HH:MM:ss Z', + ignore: 'pid,hostname', + colorize: true, + singleLine: true, + }, + }, + customLevels, + level: process.env.LOG_LEVEL ?? 'debug', + }, + production: { + customLevels, + level: process.env.LOG_LEVEL ?? 'audit', + }, + test: { + level: 'silent', + }, +}; + +@Module({ + imports: [ + PinoLoggerModule.forRootAsync({ + imports: [ConfigurationModule], + inject: [ConfigurationService], + useFactory: async (configService: ConfigurationService) => { + return { + pinoHttp: loggerConfiguration[configService.environment], + }; + }, + }), + ], + controllers: [], + providers: [], +}) +export class LoggerModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.spec.ts deleted file mode 100644 index 03ac92434..000000000 --- a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { LoggerService } from './logger.service'; - -describe('LoggerService', () => { - let service: LoggerService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [LoggerService], - }).compile(); - - service = module.get(LoggerService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.ts deleted file mode 100644 index 5c31077ab..000000000 --- a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class LoggerService {} diff --git a/apps/server-nestjs/src/main.ts b/apps/server-nestjs/src/main.ts index e5c6e0f38..d0c24014c 100644 --- a/apps/server-nestjs/src/main.ts +++ b/apps/server-nestjs/src/main.ts @@ -1,9 +1,11 @@ import { NestFactory } from '@nestjs/core'; +import { Logger } from 'nestjs-pino'; import { MainModule } from './main.module'; async function bootstrap() { - const app = await NestFactory.create(MainModule); + const app = await NestFactory.create(MainModule, { bufferLogs: true }); + app.useLogger(app.get(Logger)); await app.listen(process.env.PORT ?? 8080); } bootstrap(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2424aa238..f8c3c1fe2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -467,6 +467,12 @@ importers: mustache: specifier: ^4.2.0 version: 4.2.0 + nestjs-pino: + specifier: ^4.5.0 + version: 4.5.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) + pino-http: + specifier: ^11.0.0 + version: 11.0.0 prisma: specifier: ^6.0.1 version: 6.19.0(magicast@0.3.5)(typescript@5.9.3) @@ -6953,6 +6959,15 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-pino@4.5.0: + resolution: {integrity: sha512-e54ChJMACSGF8gPYaHsuD07RW7l/OVoV6aI8Hqhpp0ZQ4WA8QY3eewL42JX7Z1U6rV7byNU7bGBV9l6d9V6PDQ==} + engines: {node: '>= 14'} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + pino: ^7.5.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + pino-http: ^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -7311,6 +7326,9 @@ packages: pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + pino-http@11.0.0: + resolution: {integrity: sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==} + pino-pretty@13.1.2: resolution: {integrity: sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ==} hasBin: true @@ -7318,6 +7336,10 @@ packages: pino-std-serializers@7.0.0: resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + pino@10.1.0: + resolution: {integrity: sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==} + hasBin: true + pino@9.14.0: resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} hasBin: true @@ -16310,6 +16332,13 @@ snapshots: neo-async@2.6.2: {} + nestjs-pino@4.5.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): + dependencies: + '@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) + pino: 10.1.0 + pino-http: 11.0.0 + rxjs: 7.8.2 + nice-try@1.0.5: {} node-abort-controller@3.1.1: {} @@ -16668,6 +16697,13 @@ snapshots: dependencies: split2: 4.2.0 + pino-http@11.0.0: + dependencies: + get-caller-file: 2.0.5 + pino: 10.1.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + pino-pretty@13.1.2: dependencies: colorette: 2.0.20 @@ -16686,6 +16722,20 @@ snapshots: pino-std-serializers@7.0.0: {} + pino@10.1.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pino@9.14.0: dependencies: '@pinojs/redact': 0.4.0 From 77dd91cee213c01d1985de51e7c18357e9a88249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 15 Dec 2025 15:34:16 +0100 Subject: [PATCH 19/33] chore(server-nestjs): retrieve prisma directory from old-server as-is --- .../20230706084346_dso/migration.sql | 151 +++++++++++++ .../20230710181052_dso/migration.sql | 85 ++++++++ .../20230711132934_dso/migration.sql | 11 + .../20230802143822_dso/migration.sql | 10 + .../20230912084459_dso/migration.sql | 2 + .../20231010111515_dso/migration.sql | 8 + .../20231011125838_dso/migration.sql | 81 +++++++ .../20231011125839_dso/migration.sql | 35 ++++ .../20231011125841_dso/migration.sql | 36 ++++ .../20231012105520_dso/migration.sql | 11 + .../20231024155020_dso/migration.sql | 3 + .../20231026150220_dso/migration.sql | 3 + .../20240112135751_dso/migration.sql | 2 + .../20240321123436_dso/migration.sql | 12 ++ .../20240329172938_dso/migration.sql | 46 ++++ .../20240424093852_dso/migration.sql | 23 ++ .../20240427181037_dso/migration.sql | 19 ++ .../20240605135052_dso/migration.sql | 9 + .../20240612123132_dso/migration.sql | 8 + .../20240614222908_dso/migration.sql | 11 + .../20240618112205_dso/migration.sql | 58 +++++ .../20240717084709_dso/migration.sql | 9 + .../20240723135420_dso/migration.sql | 198 ++++++++++++++++++ .../20240725162050_dso/migration.sql | 5 + .../20240726210139_dso/migration.sql | 14 ++ .../20240808082632_dso/migration.sql | 17 ++ .../20240826143230_dso/migration.sql | 3 + .../20240829085548_dso/migration.sql | 12 ++ .../20240916141253_token/migration.sql | 23 ++ .../migration.sql | 8 + .../20240923142722_dso/migration.sql | 2 + .../20240923155416_dso/migration.sql | 2 + .../20240928002900_dso/migration.sql | 2 + .../migration.sql | 12 ++ .../20241104232540_add_usertype/migration.sql | 12 ++ .../20241104232541_add_pat/migration.sql | 84 ++++++++ .../migration.sql | 2 + .../20241112101945_add_slug/migration.sql | 14 ++ .../migration.sql | 2 + .../20241216131342_dso/migration.sql | 17 ++ .../20250107104749_dso/migration.sql | 2 + .../migration.sql | 25 +++ .../migration.sql | 15 ++ .../20250723141246_dso/migration.sql | 2 + .../20250818095032_remove_quota/migration.sql | 44 ++++ .../migration.sql | 5 + .../migration.sql | 9 + .../migration.sql | 4 + .../migration.sql | 4 + .../src/prisma/migrations/migration_lock.toml | 3 + .../src/prisma/schema/admin.prisma | 20 ++ .../src/prisma/schema/project.prisma | 106 ++++++++++ .../src/prisma/schema/schema.prisma | 21 ++ .../src/prisma/schema/token.prisma | 30 +++ .../src/prisma/schema/topography.prisma | 53 +++++ .../src/prisma/schema/user.prisma | 23 ++ 56 files changed, 1428 insertions(+) create mode 100644 apps/server-nestjs/src/prisma/migrations/20230706084346_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20230710181052_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20230711132934_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20230802143822_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20230912084459_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20231010111515_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20231011125838_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20231011125839_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20231011125841_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20231012105520_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20231024155020_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20231026150220_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240112135751_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240321123436_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240329172938_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240424093852_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240427181037_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240605135052_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240612123132_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240614222908_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240618112205_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240717084709_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240723135420_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240725162050_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240726210139_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240808082632_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240826143230_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240829085548_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240916141253_token/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240919122331_optional_user_id/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240923142722_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240923155416_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20240928002900_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20241008125724_enabling_maven/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20241104232540_add_usertype/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20241104232541_add_pat/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20241107142721_user_last_login/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20241112101945_add_slug/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20241216131342_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20250107104749_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20250121222954_drop_organization/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20250723141246_dso/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20250818095032_remove_quota/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20250916134454_add_project_resources/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql create mode 100644 apps/server-nestjs/src/prisma/migrations/migration_lock.toml create mode 100644 apps/server-nestjs/src/prisma/schema/admin.prisma create mode 100644 apps/server-nestjs/src/prisma/schema/project.prisma create mode 100644 apps/server-nestjs/src/prisma/schema/schema.prisma create mode 100644 apps/server-nestjs/src/prisma/schema/token.prisma create mode 100644 apps/server-nestjs/src/prisma/schema/topography.prisma create mode 100644 apps/server-nestjs/src/prisma/schema/user.prisma diff --git a/apps/server-nestjs/src/prisma/migrations/20230706084346_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20230706084346_dso/migration.sql new file mode 100644 index 000000000..f2f4e7b0b --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20230706084346_dso/migration.sql @@ -0,0 +1,151 @@ +-- CreateTable +CREATE TABLE "Environment" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "projectId" UUID NOT NULL, + "status" TEXT NOT NULL DEFAULT 'initializing', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Environment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Log" ( + "id" UUID NOT NULL, + "data" JSONB NOT NULL, + "action" TEXT NOT NULL DEFAULT '', + "userId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Log_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Organization" ( + "id" UUID NOT NULL, + "source" TEXT NOT NULL, + "name" TEXT NOT NULL, + "label" TEXT NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Permission" ( + "id" UUID NOT NULL, + "userId" UUID NOT NULL, + "environmentId" UUID NOT NULL, + "level" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Permission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Project" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "organizationId" UUID NOT NULL, + "description" TEXT, + "status" TEXT NOT NULL, + "locked" BOOLEAN NOT NULL DEFAULT false, + "services" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Repository" ( + "id" UUID NOT NULL, + "projectId" UUID NOT NULL, + "internalRepoName" TEXT NOT NULL, + "externalRepoUrl" TEXT NOT NULL, + "externalUserName" TEXT, + "externalToken" TEXT, + "isInfra" BOOLEAN NOT NULL DEFAULT false, + "isPrivate" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'initializing', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Repository_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" UUID NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Role" ( + "userId" UUID NOT NULL, + "projectId" UUID NOT NULL, + "role" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Role_pkey" PRIMARY KEY ("userId","projectId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_id_key" ON "Organization"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_name_key" ON "Organization"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_label_key" ON "Organization"("label"); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_id_key" ON "Permission"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_userId_environmentId_key" ON "Permission"("userId", "environmentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Project_id_key" ON "Project"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Role_userId_projectId_key" ON "Role"("userId", "projectId"); + +-- AddForeignKey +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Log" ADD CONSTRAINT "Log_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Permission" ADD CONSTRAINT "Permission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Permission" ADD CONSTRAINT "Permission_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Repository" ADD CONSTRAINT "Repository_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Role" ADD CONSTRAINT "Role_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/prisma/migrations/20230710181052_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20230710181052_dso/migration.sql new file mode 100644 index 000000000..26e1ade3f --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20230710181052_dso/migration.sql @@ -0,0 +1,85 @@ +-- CreateEnum +CREATE TYPE "ClusterPrivacy" AS ENUM ('public', 'dedicated'); + +-- CreateTable +CREATE TABLE "Cluster" ( + "id" UUID NOT NULL, + "label" VARCHAR(50) NOT NULL, + "privacy" "ClusterPrivacy" NOT NULL DEFAULT 'dedicated', + "secretName" VARCHAR(50) NOT NULL, + "clusterResources" BOOLEAN NOT NULL DEFAULT false, + "kubeConfigId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Cluster_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Kubeconfig" ( + "id" UUID NOT NULL, + "user" JSONB NOT NULL, + "cluster" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "parentClusterId" UUID, + + CONSTRAINT "Kubeconfig_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_ClusterToEnvironment" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_ClusterToProject" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Cluster_id_key" ON "Cluster"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Cluster_label_key" ON "Cluster"("label"); + +-- CreateIndex +CREATE UNIQUE INDEX "Cluster_secretName_key" ON "Cluster"("secretName"); + +-- CreateIndex +CREATE UNIQUE INDEX "Cluster_kubeConfigId_key" ON "Cluster"("kubeConfigId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Kubeconfig_id_key" ON "Kubeconfig"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Kubeconfig_parentClusterId_key" ON "Kubeconfig"("parentClusterId"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ClusterToEnvironment_AB_unique" ON "_ClusterToEnvironment"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ClusterToEnvironment_B_index" ON "_ClusterToEnvironment"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ClusterToProject_AB_unique" ON "_ClusterToProject"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ClusterToProject_B_index" ON "_ClusterToProject"("B"); + +-- AddForeignKey +ALTER TABLE "Cluster" ADD CONSTRAINT "Cluster_kubeConfigId_fkey" FOREIGN KEY ("kubeConfigId") REFERENCES "Kubeconfig"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToEnvironment" ADD CONSTRAINT "_ClusterToEnvironment_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToEnvironment" ADD CONSTRAINT "_ClusterToEnvironment_B_fkey" FOREIGN KEY ("B") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_B_fkey" FOREIGN KEY ("B") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/prisma/migrations/20230711132934_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20230711132934_dso/migration.sql new file mode 100644 index 000000000..8f3fb5ff9 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20230711132934_dso/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `parentClusterId` on the `Kubeconfig` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "Kubeconfig_parentClusterId_key"; + +-- AlterTable +ALTER TABLE "Kubeconfig" DROP COLUMN "parentClusterId"; diff --git a/apps/server-nestjs/src/prisma/migrations/20230802143822_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20230802143822_dso/migration.sql new file mode 100644 index 000000000..4eb37edc5 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20230802143822_dso/migration.sql @@ -0,0 +1,10 @@ +CREATE TYPE "ProjectStatus" AS ENUM ('initializing', 'created', 'failed', 'archived'); + +ALTER TABLE public."Project" ALTER COLUMN status TYPE "ProjectStatus" USING + case + when status = 'created' then 'created'::"ProjectStatus" + when status = 'failed' then 'failed'::"ProjectStatus" + when status = 'archived' then 'archived'::"ProjectStatus" + else 'initializing'::"ProjectStatus" + end; +ALTER TABLE public."Project" ALTER COLUMN status SET DEFAULT 'initializing'; diff --git a/apps/server-nestjs/src/prisma/migrations/20230912084459_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20230912084459_dso/migration.sql new file mode 100644 index 000000000..f402a0e3d --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20230912084459_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Cluster" ADD COLUMN "infos" VARCHAR(200); diff --git a/apps/server-nestjs/src/prisma/migrations/20231010111515_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20231010111515_dso/migration.sql new file mode 100644 index 000000000..f553e7880 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20231010111515_dso/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `externalToken` on the `Repository` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Repository" DROP COLUMN "externalToken"; diff --git a/apps/server-nestjs/src/prisma/migrations/20231011125838_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20231011125838_dso/migration.sql new file mode 100644 index 000000000..394b650a5 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20231011125838_dso/migration.sql @@ -0,0 +1,81 @@ +-- CreateEnum +CREATE TYPE "QuotaStageStatus" AS ENUM +('active', 'pendingDelete'); + +-- Create new tables +-- CreateTable +CREATE TABLE "Quota" +( + "id" UUID NOT NULL, + "memory" VARCHAR NOT NULL, + "cpu" REAL NOT NULL, + "name" VARCHAR NOT NULL, + "isPrivate" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "Quota_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Stage" +( + "id" UUID NOT NULL, + "name" VARCHAR NOT NULL, + + CONSTRAINT "Stage_pkey" PRIMARY KEY ("id") +); + +-- Associate Quotas and Stages +-- CreateTable +CREATE TABLE "QuotaStage" +( + "id" UUID NOT NULL, + "quotaId" UUID NOT NULL, + "stageId" UUID NOT NULL, + "status" "QuotaStageStatus" NOT NULL DEFAULT 'active', + + CONSTRAINT "QuotaStage_pkey" PRIMARY KEY ("id") +); +CREATE UNIQUE INDEX "Quota_id_key" ON "Quota"("id"); +CREATE UNIQUE INDEX "Quota_name_key" ON "Quota"("name"); +CREATE UNIQUE INDEX "Stage_id_key" ON "Stage"("id"); +CREATE UNIQUE INDEX "Stage_name_key" ON "Stage"("name"); +CREATE UNIQUE INDEX "QuotaStage_id_key" ON "QuotaStage"("id"); +CREATE UNIQUE INDEX "QuotaStage_quotaId_stageId_key" ON "QuotaStage"("quotaId", "stageId"); +ALTER TABLE "QuotaStage" ADD CONSTRAINT "QuotaStage_quotaId_fkey" FOREIGN KEY ("quotaId") REFERENCES "Quota"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "QuotaStage" ADD CONSTRAINT "QuotaStage_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- Create default values for Quotas and Stages +-- Quota +INSERT INTO "Quota" + (id, cpu, memory, "name", "isPrivate") +VALUES + ('5a57b62f-2465-4fb6-a853-5a751d099199', 2, '4Gi', 'small', false), + ('08770663-3b76-4af6-8978-9f75eda4faa7', 4, '8Gi', 'medium', false), + ('b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', 6, '12Gi', 'large', false), + ('97b851e8-9067-4a3d-a0e8-c3a6820c49be', 8, '16Gi', 'xlarge', false); + +-- Stage +INSERT INTO "Stage" + (id, "name") +VALUES + ('4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', 'dev'), + ('38fa869d-6267-441d-af7f-e0548fd06b7e', 'staging'), + ('d434310e-7850-4d59-b47f-0772edf50582', 'integration'), + ('9b3e9991-896d-4d90-bdc5-a34be8c06b8f', 'prod'); + +-- QuotaStage +INSERT INTO "QuotaStage" + (id, "quotaId", "stageId") +VALUES + ('0cb0c549-560e-4f26-8f4e-832dd722f68a', '5a57b62f-2465-4fb6-a853-5a751d099199', '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), + ('0530e9c9-b37d-4dec-93e6-1895f700e61c', '5a57b62f-2465-4fb6-a853-5a751d099199', '38fa869d-6267-441d-af7f-e0548fd06b7e'), + ('8a99db49-b7b1-44bf-865d-5e709e8aa0fc', '5a57b62f-2465-4fb6-a853-5a751d099199', 'd434310e-7850-4d59-b47f-0772edf50582'), + ('67561f00-d219-4ca6-b94a-3ee83f09d2d6', '5a57b62f-2465-4fb6-a853-5a751d099199', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), + ('8b3c201e-7518-4254-a94a-16c404e46936', '08770663-3b76-4af6-8978-9f75eda4faa7', '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), + ('9157ae12-3e39-43f8-a24f-ae5d9c6b69b7', '08770663-3b76-4af6-8978-9f75eda4faa7', '38fa869d-6267-441d-af7f-e0548fd06b7e'), + ('c733a1dd-c9fd-4def-b29e-df49ef7b6698', '08770663-3b76-4af6-8978-9f75eda4faa7', 'd434310e-7850-4d59-b47f-0772edf50582'), + ('15a51f47-0ab2-4a94-a808-722639d8c092', '08770663-3b76-4af6-8978-9f75eda4faa7', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), + ('cb66e80c-2304-472d-bc19-a411011674ca', 'b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', 'd434310e-7850-4d59-b47f-0772edf50582'), + ('59fb0e79-3a76-4b96-81d4-63f4caa98cfa', 'b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), + ('4174b22c-2bee-4f4a-9d85-da7b5463f214', '97b851e8-9067-4a3d-a0e8-c3a6820c49be', 'd434310e-7850-4d59-b47f-0772edf50582'), + ('de0589b6-7cf5-4f1e-ab44-53e71a6cdb7a', '97b851e8-9067-4a3d-a0e8-c3a6820c49be', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'); diff --git a/apps/server-nestjs/src/prisma/migrations/20231011125839_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20231011125839_dso/migration.sql new file mode 100644 index 000000000..8c98a7f74 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20231011125839_dso/migration.sql @@ -0,0 +1,35 @@ +-- Multiplication des environnements par clusteurs +ALTER TABLE "Environment" ADD COLUMN "clusterId" UUID; + +DO +$$ +DECLARE + perm record; + cte record; + env_uuid UUID; +BEGIN + FOR cte IN SELECT "B" AS environmentId, "A" AS "clusterId", "name", "projectId", status, "updatedAt", "createdAt" + FROM public."_ClusterToEnvironment", public."Environment" + WHERE public."Environment".id = "B" + LOOP + env_uuid := gen_random_uuid(); + INSERT INTO public."Environment" (id, "name", "projectId", "clusterId", status, "createdAt", "updatedAt") VALUES + (env_uuid, cte."name", cte."projectId", cte."clusterId", cte.status, cte."createdAt", cte."updatedAt"); + + FOR perm in SELECT * FROM public."Permission" WHERE "environmentId" = cte.environmentId + LOOP + INSERT INTO public."Permission" (id, "level", "createdAt", "updatedAt", "environmentId", "userId") VALUES + (gen_random_uuid(), perm."level", perm."createdAt", perm."updatedAt", env_uuid, perm."userId"); + END LOOP; + END LOOP; +END; +$$ +; +DELETE FROM public."Environment" WHERE "clusterId" is null; +ALTER TABLE public."Environment" ALTER COLUMN "clusterId" SET NOT NULL; +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_clusterId_fkey" FOREIGN KEY ("clusterId") REFERENCES "Cluster"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Delete old _ClusterToEnvironment +ALTER TABLE "_ClusterToEnvironment" DROP CONSTRAINT "_ClusterToEnvironment_A_fkey"; +ALTER TABLE "_ClusterToEnvironment" DROP CONSTRAINT "_ClusterToEnvironment_B_fkey"; +DROP TABLE "_ClusterToEnvironment"; diff --git a/apps/server-nestjs/src/prisma/migrations/20231011125841_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20231011125841_dso/migration.sql new file mode 100644 index 000000000..035cd1c85 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20231011125841_dso/migration.sql @@ -0,0 +1,36 @@ +-- Associate cluster to Stages +-- CreateTable +CREATE TABLE "_ClusterToStage" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); +-- AddForeignKey +ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_B_fkey" FOREIGN KEY ("B") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- CreateIndex +CREATE UNIQUE INDEX "_ClusterToStage_AB_unique" ON "_ClusterToStage"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ClusterToStage_B_index" ON "_ClusterToStage"("B"); + +DO +$$ +DECLARE + cluster record; + cte record; + env_uuid UUID; +BEGIN + FOR cluster IN SELECT id + FROM public."Cluster" + LOOP + INSERT INTO public."_ClusterToStage" ("A", "B") VALUES + (cluster.id, '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), + (cluster.id, '38fa869d-6267-441d-af7f-e0548fd06b7e'), + (cluster.id, 'd434310e-7850-4d59-b47f-0772edf50582'), + (cluster.id, '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'); + END LOOP; +END; +$$ diff --git a/apps/server-nestjs/src/prisma/migrations/20231012105520_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20231012105520_dso/migration.sql new file mode 100644 index 000000000..43793bdb4 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20231012105520_dso/migration.sql @@ -0,0 +1,11 @@ +-- AlterTable +ALTER TABLE "Environment" ADD COLUMN "quotaStageId" UUID; +UPDATE "Environment" SET "quotaStageId" = '8b3c201e-7518-4254-a94a-16c404e46936' WHERE "name" = 'dev'; +UPDATE "Environment" SET "quotaStageId" = '9157ae12-3e39-43f8-a24f-ae5d9c6b69b7' WHERE "name" = 'staging'; +UPDATE "Environment" SET "quotaStageId" = '4174b22c-2bee-4f4a-9d85-da7b5463f214' WHERE "name" = 'integration'; +UPDATE "Environment" SET "quotaStageId" = 'de0589b6-7cf5-4f1e-ab44-53e71a6cdb7a' WHERE "name" = 'prod'; +ALTER TABLE "Environment" ALTER COLUMN "name" SET DATA TYPE VARCHAR(11); +ALTER TABLE "Environment" ALTER COLUMN "quotaStageId" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_quotaStageId_fkey" FOREIGN KEY ("quotaStageId") REFERENCES "QuotaStage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/prisma/migrations/20231024155020_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20231024155020_dso/migration.sql new file mode 100644 index 000000000..9af004b6a --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20231024155020_dso/migration.sql @@ -0,0 +1,3 @@ +-- Please read 6.0.0 Release notes ! +-- lock all projects +UPDATE public."Project" SET "locked"=true \ No newline at end of file diff --git a/apps/server-nestjs/src/prisma/migrations/20231026150220_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20231026150220_dso/migration.sql new file mode 100644 index 000000000..d970d3965 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20231026150220_dso/migration.sql @@ -0,0 +1,3 @@ +-- Please read 6.0.0 Release notes ! +-- set all projects to failed to avoid unlock them +UPDATE public."Project" SET "status" = 'failed' WHERE "status" != 'archived' \ No newline at end of file diff --git a/apps/server-nestjs/src/prisma/migrations/20240112135751_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240112135751_dso/migration.sql new file mode 100644 index 000000000..c387d9885 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240112135751_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Log" ADD COLUMN "requestId" VARCHAR(21); diff --git a/apps/server-nestjs/src/prisma/migrations/20240321123436_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240321123436_dso/migration.sql new file mode 100644 index 000000000..18e20262c --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240321123436_dso/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `status` on the `Environment` table. All the data in the column will be lost. + - You are about to drop the column `status` on the `Repository` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Environment" DROP COLUMN "status"; + +-- AlterTable +ALTER TABLE "Repository" DROP COLUMN "status"; diff --git a/apps/server-nestjs/src/prisma/migrations/20240329172938_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240329172938_dso/migration.sql new file mode 100644 index 000000000..f784b2156 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240329172938_dso/migration.sql @@ -0,0 +1,46 @@ +-- AlterTable +ALTER TABLE "Cluster" ADD COLUMN "zoneId" UUID; + +-- CreateTable +CREATE TABLE "Zone" +( + "id" UUID NOT NULL, + "slug" VARCHAR(10) NOT NULL, + "label" VARCHAR(50) NOT NULL, + "description" VARCHAR(200), + "createdAt" TIMESTAMP +(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP +(3) NOT NULL, + + CONSTRAINT "Zone_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Zone_id_key" ON "Zone"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Zone_slug_key" ON "Zone"("slug"); + +-- Create default zone +INSERT INTO "Zone" + (id, "slug", "label", "description", "updatedAt") +VALUES + ('a66c4230-eba6-41f1-aae5-bb1e4f90cce0', 'default', 'Zone Défaut', 'Zone par défaut, à changer', CURRENT_TIMESTAMP); + +-- Set default zoneId for current clusters +UPDATE "Cluster" +SET "zoneId" += 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0' +WHERE "zoneId" +IS NULL; + +-- AlterTable +ALTER TABLE "Cluster" ALTER COLUMN "zoneId" +SET +NOT NULL; + +-- AddForeignKey +ALTER TABLE "Cluster" ADD CONSTRAINT "Cluster_zoneId_fkey" FOREIGN KEY ("zoneId") REFERENCES "Zone"("id") +ON DELETE RESTRICT ON +UPDATE CASCADE; diff --git a/apps/server-nestjs/src/prisma/migrations/20240424093852_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240424093852_dso/migration.sql new file mode 100644 index 000000000..cb4f7ad96 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240424093852_dso/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "ProjectPlugin" ( + "pluginName" TEXT NOT NULL, + "projectId" UUID NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "AdminPlugin" ( + "pluginName" TEXT NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectPlugin_projectId_pluginName_key_key" ON "ProjectPlugin"("projectId", "pluginName", "key"); + +-- CreateIndex +CREATE UNIQUE INDEX "AdminPlugin_pluginName_key_key" ON "AdminPlugin"("pluginName", "key"); + +-- AddForeignKey +ALTER TABLE "ProjectPlugin" ADD CONSTRAINT "ProjectPlugin_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/prisma/migrations/20240427181037_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240427181037_dso/migration.sql new file mode 100644 index 000000000..11672324f --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240427181037_dso/migration.sql @@ -0,0 +1,19 @@ +DO $$ +DECLARE + project_row RECORD; + registry_id INT; +BEGIN + -- Début de la boucle sur chaque ligne de la table 'Project' + FOR project_row IN SELECT id, services FROM public."Project" LOOP + -- Extrait 'registry.id' de la colonne JSON 'services' + registry_id := (SELECT (project_row.services -> 'registry' ->> 'id')::TEXT); + -- Si 'registry.id' existe, insérer dans la table 'config' + IF registry_id IS NOT NULL THEN + INSERT INTO public."ProjectPlugin" ("projectId", "pluginName", "key", "value") + VALUES (project_row.id, 'registry', 'projectId', registry_id::TEXT); + END IF; + END LOOP; +END $$; + +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "services"; diff --git a/apps/server-nestjs/src/prisma/migrations/20240605135052_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240605135052_dso/migration.sql new file mode 100644 index 000000000..9c7d5a8f8 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240605135052_dso/migration.sql @@ -0,0 +1,9 @@ +-- CreateEnum +CREATE TYPE "RoleList" AS ENUM ('owner', 'user'); + +-- AlterTable +ALTER TABLE public."Role" ALTER COLUMN "role" TYPE "RoleList" USING + case + when role = 'owner' then 'owner'::"RoleList" + else 'user'::"RoleList" + end; \ No newline at end of file diff --git a/apps/server-nestjs/src/prisma/migrations/20240612123132_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240612123132_dso/migration.sql new file mode 100644 index 000000000..45a4a5d1e --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240612123132_dso/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "ProjectClusterHistory" ( + "projectId" UUID NOT NULL, + "clusterId" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "ProjectClusterHistory_projectId_clusterId_key" ON "ProjectClusterHistory"("projectId", "clusterId"); diff --git a/apps/server-nestjs/src/prisma/migrations/20240614222908_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240614222908_dso/migration.sql new file mode 100644 index 000000000..2b1641a65 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240614222908_dso/migration.sql @@ -0,0 +1,11 @@ +DO $$ +DECLARE + env_row RECORD; +BEGIN + -- Début de la boucle sur chaque ligne de la table 'Project' + FOR env_row IN SELECT "projectId", "clusterId" FROM public."Environment" LOOP + INSERT INTO public."ProjectClusterHistory" ("projectId", "clusterId") + VALUES (env_row."projectId", env_row."clusterId") + ON CONFLICT DO NOTHING; + END LOOP; +END $$; \ No newline at end of file diff --git a/apps/server-nestjs/src/prisma/migrations/20240618112205_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240618112205_dso/migration.sql new file mode 100644 index 000000000..5e7ff03d4 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240618112205_dso/migration.sql @@ -0,0 +1,58 @@ +-- AlterTable +ALTER TABLE "Environment" ADD COLUMN "quotaId" UUID, +ADD COLUMN "stageId" UUID; + +-- AddForeignKey +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_quotaId_fkey" FOREIGN KEY ("quotaId") REFERENCES "Quota"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Environment" ADD CONSTRAINT "Environment_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE "_QuotaToStage" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_QuotaToStage_AB_unique" ON "_QuotaToStage"("A", "B"); + +-- CreateIndex +CREATE INDEX "_QuotaToStage_B_index" ON "_QuotaToStage"("B"); + +-- AddForeignKey +ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_A_fkey" FOREIGN KEY ("A") REFERENCES "Quota"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_B_fkey" FOREIGN KEY ("B") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +DO $$ +DECLARE + quota_stage_row RECORD; +BEGIN + FOR quota_stage_row IN SELECT * FROM public."QuotaStage" loop + UPDATE public."Environment" SET "stageId" = quota_stage_row."stageId" WHERE "Environment"."quotaStageId" = quota_stage_row.id; + UPDATE public."Environment" SET "quotaId" = quota_stage_row."quotaId" WHERE "Environment"."quotaStageId" = quota_stage_row.id; + insert into public."_QuotaToStage" values (quota_stage_row."quotaId", quota_stage_row."stageId"); + END LOOP; +END $$; + +-- DropForeignKey +ALTER TABLE "Environment" DROP CONSTRAINT "Environment_quotaStageId_fkey"; + +-- AlterTable +ALTER TABLE "Environment" ALTER COLUMN "quotaId" SET NOT NULL, +ALTER COLUMN "stageId" SET NOT NULL, +DROP COLUMN "quotaStageId"; + +-- DropForeignKey +ALTER TABLE "QuotaStage" DROP CONSTRAINT "QuotaStage_quotaId_fkey"; + +-- DropForeignKey +ALTER TABLE "QuotaStage" DROP CONSTRAINT "QuotaStage_stageId_fkey"; + +-- DropTable +DROP TABLE "QuotaStage"; + +-- DropEnum +DROP TYPE "QuotaStageStatus"; diff --git a/apps/server-nestjs/src/prisma/migrations/20240717084709_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240717084709_dso/migration.sql new file mode 100644 index 000000000..0036da8a1 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240717084709_dso/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Made the column `description` on table `Project` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Project" ALTER COLUMN "description" SET NOT NULL, +ALTER COLUMN "description" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/prisma/migrations/20240723135420_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240723135420_dso/migration.sql new file mode 100644 index 000000000..ed6ae9b84 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240723135420_dso/migration.sql @@ -0,0 +1,198 @@ +-- DropForeignKey if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Permission_environmentId_fkey') THEN + ALTER TABLE "Permission" DROP CONSTRAINT "Permission_environmentId_fkey"; + END IF; +END $$; + +-- DropForeignKey if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Permission_userId_fkey') THEN + ALTER TABLE "Permission" DROP CONSTRAINT "Permission_userId_fkey"; + END IF; +END $$; + +-- DropForeignKey if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Role_projectId_fkey') THEN + ALTER TABLE "Role" DROP CONSTRAINT "Role_projectId_fkey"; + END IF; +END $$; + +-- DropForeignKey if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Role_userId_fkey') THEN + ALTER TABLE "Role" DROP CONSTRAINT "Role_userId_fkey"; + END IF; +END $$; + +-- CreateTable if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ProjectMembers') THEN + CREATE TABLE "ProjectMembers" ( + "projectId" UUID NOT NULL, + "userId" UUID NOT NULL, + "roleIds" TEXT[] + ); + END IF; +END $$; + +-- AlterTable +ALTER TABLE "Log" ADD COLUMN IF NOT EXISTS "projectId" UUID; + +INSERT INTO public."User" (id, "firstName", "lastName", email, "createdAt", "updatedAt") +VALUES('04ac168a-2c4f-4816-9cce-af6c612e5912'::uuid, 'Anonymous', 'User', 'anon@user', '2023-07-03 14:46:56.770', '2023-07-03 14:46:56.770') +ON CONFLICT (id) DO NOTHING; + +-- AlterTable +ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "everyonePerms" BIGINT NOT NULL DEFAULT 896, +ADD COLUMN IF NOT EXISTS "ownerId" UUID; + +DO $$ +DECLARE + role_row RECORD; +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Role') THEN + -- Début de la boucle sur chaque ligne de la table 'Project' + FOR role_row IN SELECT "userId", "projectId", "role" FROM public."Role" LOOP + INSERT INTO public."ProjectMembers" ("userId", "projectId", "roleIds") VALUES (role_row."userId", role_row."projectId", '{}'); + IF role_row."role" = 'owner'::public."RoleList" THEN + UPDATE public."Project" + SET "ownerId"=role_row."userId" + WHERE id=role_row."projectId"::uuid; + END IF; + END LOOP; + END IF; +END $$; + +UPDATE public."Project" +SET "ownerId"='04ac168a-2c4f-4816-9cce-af6c612e5912' +WHERE "ownerId" IS NULL; + +ALTER TABLE public."Project" ALTER COLUMN "ownerId" SET NOT NULL; + +DELETE FROM public."ProjectMembers" pm +USING public."Project" p +WHERE pm."userId" = p."ownerId" +AND pm."projectId" = p."id"; + +-- DropTable if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Permission') THEN + DROP TABLE "Permission"; + END IF; +END $$; + +-- DropTable if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Role') THEN + DROP TABLE "Role"; + END IF; +END $$; + +-- DropEnum if exists +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'RoleList') THEN + DROP TYPE "RoleList"; + END IF; +END $$; + +-- CreateTable if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'AdminRole') THEN + CREATE TABLE "AdminRole" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "permissions" BIGINT NOT NULL, + "position" SMALLINT NOT NULL, + CONSTRAINT "AdminRole_pkey" PRIMARY KEY ("id") + ); + END IF; +END $$; + +-- CreateTable if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ProjectRole') THEN + CREATE TABLE "ProjectRole" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "permissions" BIGINT NOT NULL, + "projectId" UUID NOT NULL, + "position" SMALLINT NOT NULL, + CONSTRAINT "ProjectRole_pkey" PRIMARY KEY ("id") + ); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'AdminRole_id_key') THEN + CREATE UNIQUE INDEX "AdminRole_id_key" ON "AdminRole"("id"); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'AdminRole_name_key') THEN + CREATE UNIQUE INDEX "AdminRole_name_key" ON "AdminRole"("name"); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'ProjectMembers_projectId_userId_key') THEN + CREATE UNIQUE INDEX "ProjectMembers_projectId_userId_key" ON "ProjectMembers"("projectId", "userId"); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'ProjectRole_id_key') THEN + CREATE UNIQUE INDEX "ProjectRole_id_key" ON "ProjectRole"("id"); + END IF; +END $$; + +-- CreateIndex if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'Environment_projectId_name_key') THEN + CREATE UNIQUE INDEX "Environment_projectId_name_key" ON "Environment"("projectId", "name"); + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Log_projectId_fkey') THEN + ALTER TABLE "Log" ADD CONSTRAINT "Log_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Project_ownerId_fkey') THEN + ALTER TABLE "Project" ADD CONSTRAINT "Project_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectMembers_projectId_fkey') THEN + ALTER TABLE "ProjectMembers" ADD CONSTRAINT "ProjectMembers_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectMembers_userId_fkey') THEN + ALTER TABLE "ProjectMembers" ADD CONSTRAINT "ProjectMembers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- AddForeignKey if not exists +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectRole_projectId_fkey') THEN + ALTER TABLE "ProjectRole" ADD CONSTRAINT "ProjectRole_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "adminRoleIds" TEXT[]; diff --git a/apps/server-nestjs/src/prisma/migrations/20240725162050_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240725162050_dso/migration.sql new file mode 100644 index 000000000..c9b41827b --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240725162050_dso/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "AdminRole_name_key"; + +-- AlterTable +ALTER TABLE "AdminRole" ADD COLUMN "oidcGroup" TEXT; diff --git a/apps/server-nestjs/src/prisma/migrations/20240726210139_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240726210139_dso/migration.sql new file mode 100644 index 000000000..265f262ab --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240726210139_dso/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - Made the column `oidcGroup` on table `AdminRole` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable + +UPDATE public."AdminRole" +SET "oidcGroup"='' +WHERE "oidcGroup" IS NULL; + +ALTER TABLE "AdminRole" ALTER COLUMN "oidcGroup" SET NOT NULL, +ALTER COLUMN "oidcGroup" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/prisma/migrations/20240808082632_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240808082632_dso/migration.sql new file mode 100644 index 000000000..4fc276860 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240808082632_dso/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "SystemSetting" +( + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + + CONSTRAINT "SystemSetting_pkey" PRIMARY KEY ("key") +); + +-- CreateIndex +CREATE UNIQUE INDEX "SystemSetting_key_key" ON "SystemSetting"("key"); + +-- Create maintenance setting +INSERT INTO "SystemSetting" + ("key", "value") +VALUES + ('maintenance', 'off'); diff --git a/apps/server-nestjs/src/prisma/migrations/20240826143230_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240826143230_dso/migration.sql new file mode 100644 index 000000000..95ab54869 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240826143230_dso/migration.sql @@ -0,0 +1,3 @@ +INSERT INTO public."AdminRole" +(id, "name", permissions, "position", "oidcGroup") +VALUES('76229c96-4716-45bc-99da-00498ec9018c'::uuid, 'Admin', 2, 0, '/admin'); \ No newline at end of file diff --git a/apps/server-nestjs/src/prisma/migrations/20240829085548_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240829085548_dso/migration.sql new file mode 100644 index 000000000..c11648218 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240829085548_dso/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Made the column `externalUserName` on table `Repository` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +UPDATE "Repository" SET "externalUserName" = '' WHERE "externalUserName" IS NULL; + +ALTER TABLE "Repository" ALTER COLUMN "externalUserName" SET NOT NULL, +ALTER COLUMN "externalUserName" SET DEFAULT '', +ALTER COLUMN "externalRepoUrl" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/prisma/migrations/20240916141253_token/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240916141253_token/migration.sql new file mode 100644 index 000000000..b0472cd80 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240916141253_token/migration.sql @@ -0,0 +1,23 @@ +-- CreateEnum +CREATE TYPE "TokenStatus" AS ENUM ('active', 'revoked'); + +-- CreateTable +CREATE TABLE "AdminToken" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "permissions" BIGINT NOT NULL, + "userId" UUID, + "expirationDate" TIMESTAMP(3), + "lastUse" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" "TokenStatus" NOT NULL DEFAULT 'active', + "hash" TEXT NOT NULL, + + CONSTRAINT "AdminToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AdminToken_id_key" ON "AdminToken"("id"); + +-- AddForeignKey +ALTER TABLE "AdminToken" ADD CONSTRAINT "AdminToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/prisma/migrations/20240919122331_optional_user_id/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240919122331_optional_user_id/migration.sql new file mode 100644 index 000000000..47488b00c --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240919122331_optional_user_id/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Log" DROP CONSTRAINT "Log_userId_fkey"; + +-- AlterTable +ALTER TABLE "Log" ALTER COLUMN "userId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Log" ADD CONSTRAINT "Log_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/prisma/migrations/20240923142722_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240923142722_dso/migration.sql new file mode 100644 index 000000000..18eca3ead --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240923142722_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Log" ALTER COLUMN "requestId" SET DATA TYPE VARCHAR(36); diff --git a/apps/server-nestjs/src/prisma/migrations/20240923155416_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240923155416_dso/migration.sql new file mode 100644 index 000000000..74e0946f0 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240923155416_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Zone" ADD COLUMN "argocdUrl" TEXT NOT NULL DEFAULT 'https://example.com'; diff --git a/apps/server-nestjs/src/prisma/migrations/20240928002900_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20240928002900_dso/migration.sql new file mode 100644 index 000000000..41dac7535 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20240928002900_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "ProjectStatus" ADD VALUE 'warning'; diff --git a/apps/server-nestjs/src/prisma/migrations/20241008125724_enabling_maven/migration.sql b/apps/server-nestjs/src/prisma/migrations/20241008125724_enabling_maven/migration.sql new file mode 100644 index 000000000..ef888d5e5 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20241008125724_enabling_maven/migration.sql @@ -0,0 +1,12 @@ +DO $$ +DECLARE + project_row RECORD; + registry_id INT; +BEGIN + -- Début de la boucle sur chaque ligne de la table 'Project' + FOR project_row IN SELECT id FROM public."Project" WHERE status <> 'archived'::public."ProjectStatus" LOOP + INSERT INTO public."ProjectPlugin" ("projectId", "pluginName", "key", "value") + VALUES (project_row.id, 'nexus', 'activateMavenRepo', 'enabled') + ON CONFLICT DO NOTHING; + END LOOP; +END $$; \ No newline at end of file diff --git a/apps/server-nestjs/src/prisma/migrations/20241104232540_add_usertype/migration.sql b/apps/server-nestjs/src/prisma/migrations/20241104232540_add_usertype/migration.sql new file mode 100644 index 000000000..a57e5956c --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20241104232540_add_usertype/migration.sql @@ -0,0 +1,12 @@ +-- CreateEnum +CREATE TYPE "UserType" AS ENUM ('human', 'bot', 'ghost'); + +-- AlterEnum +ALTER TYPE "TokenStatus" ADD VALUE 'inactive'; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "type" "UserType" NOT NULL DEFAULT 'human'; +UPDATE "User" SET type = 'ghost' WHERE id = '04ac168a-2c4f-4816-9cce-af6c612e5912'; + +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "type" DROP DEFAULT; diff --git a/apps/server-nestjs/src/prisma/migrations/20241104232541_add_pat/migration.sql b/apps/server-nestjs/src/prisma/migrations/20241104232541_add_pat/migration.sql new file mode 100644 index 000000000..71e15a312 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20241104232541_add_pat/migration.sql @@ -0,0 +1,84 @@ +-- CreateTable (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'PersonalAccessToken') THEN + CREATE TABLE "PersonalAccessToken" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "userId" UUID NOT NULL, + "expirationDate" TIMESTAMP(3) NOT NULL, + "lastUse" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" "TokenStatus" NOT NULL DEFAULT 'active', + "hash" TEXT NOT NULL, + + CONSTRAINT "PersonalAccessToken_pkey" PRIMARY KEY ("id") + ); + END IF; +END $$; + +-- CreateIndex (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'PersonalAccessToken_id_key') THEN + CREATE UNIQUE INDEX "PersonalAccessToken_id_key" ON "PersonalAccessToken"("id"); + END IF; +END $$; + +-- AddForeignKey (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'PersonalAccessToken_userId_fkey') THEN + ALTER TABLE "PersonalAccessToken" ADD CONSTRAINT "PersonalAccessToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; + +-- Process AdminToken (idempotent) +DO $$ +DECLARE + admin_token record; + user_uuid UUID; +BEGIN + FOR admin_token IN SELECT "name", "id" + FROM public."AdminToken" + LOOP + -- Generate new UUID if user does not exist + user_uuid := COALESCE( + (SELECT id FROM public."User" WHERE email = concat(admin_token.name, '@bot.id')), + gen_random_uuid() + ); + + -- Insert user if not already exists + INSERT INTO public."User" (id, "firstName", "lastName", email, "createdAt", "updatedAt", "type") + VALUES(user_uuid, 'Bot Admin', admin_token.name, concat(admin_token.name, '@bot.id'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'bot') + ON CONFLICT (id) DO NOTHING; + + -- Update AdminToken with the new user ID + UPDATE public."AdminToken" SET "userId" = user_uuid WHERE id = admin_token.id; + END LOOP; +END $$; + +-- Alter AdminToken userId column to NOT NULL (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'AdminToken' AND column_name = 'userId' AND is_nullable = 'NO') THEN + ALTER TABLE public."AdminToken" ALTER COLUMN "userId" SET NOT NULL; + END IF; +END $$; + +-- DropForeignKey if exists (idempotent) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'AdminToken_userId_fkey') THEN + ALTER TABLE "AdminToken" DROP CONSTRAINT "AdminToken_userId_fkey"; + END IF; +END $$; + +-- AddForeignKey (idempotent) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'AdminToken_userId_fkey') THEN + ALTER TABLE "AdminToken" ADD CONSTRAINT "AdminToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; diff --git a/apps/server-nestjs/src/prisma/migrations/20241107142721_user_last_login/migration.sql b/apps/server-nestjs/src/prisma/migrations/20241107142721_user_last_login/migration.sql new file mode 100644 index 000000000..521b2b10a --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20241107142721_user_last_login/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "lastLogin" TIMESTAMP(3); diff --git a/apps/server-nestjs/src/prisma/migrations/20241112101945_add_slug/migration.sql b/apps/server-nestjs/src/prisma/migrations/20241112101945_add_slug/migration.sql new file mode 100644 index 000000000..a7500833b --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20241112101945_add_slug/migration.sql @@ -0,0 +1,14 @@ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "slug" TEXT; + +UPDATE public."Project" p +SET slug = ( + SELECT concat(org.name, '-', subp.name) FROM public."Project" subp + LEFT JOIN public."Organization" org on org."id" = subp."organizationId" + WHERE subp.id = p.id +); + +ALTER TABLE public."Project" ALTER COLUMN "slug" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug"); diff --git a/apps/server-nestjs/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql b/apps/server-nestjs/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql new file mode 100644 index 000000000..b143cbeb9 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "lastSuccessProvisionningVersion" TEXT; diff --git a/apps/server-nestjs/src/prisma/migrations/20241216131342_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20241216131342_dso/migration.sql new file mode 100644 index 000000000..7a8868190 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20241216131342_dso/migration.sql @@ -0,0 +1,17 @@ +-- AlterTable +ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "_ClusterToProject_AB_unique"; + +-- AlterTable +ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "_ClusterToStage_AB_unique"; + +-- AlterTable +ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_AB_pkey" PRIMARY KEY ("A", "B"); + +-- DropIndex +DROP INDEX "_QuotaToStage_AB_unique"; diff --git a/apps/server-nestjs/src/prisma/migrations/20250107104749_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20250107104749_dso/migration.sql new file mode 100644 index 000000000..21ce77b8d --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20250107104749_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Cluster" ADD COLUMN "external" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/server-nestjs/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql b/apps/server-nestjs/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql new file mode 100644 index 000000000..ac63cd639 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql @@ -0,0 +1,25 @@ +-- Vérifie les versions dans la table Project +DO $$ +DECLARE + project_id TEXT; + project_name TEXT; + last_version TEXT; +BEGIN + -- Boucle sur les projets non archivés + FOR project_id, project_name, last_version IN ( + SELECT id, name, "lastSuccessProvisionningVersion" + FROM "Project" + WHERE "status" != 'archived' + ) + LOOP + -- Vérifie si la version est NULL + IF last_version IS NULL THEN + RAISE EXCEPTION 'Le projet % (ID: %) a une version NULL.', project_name, project_id; + END IF; + + -- Vérifie si la version est inférieure à 8.23.0 selon SemVer + IF (string_to_array(last_version, '.')::int[] < ARRAY[8,23,0]) THEN + RAISE EXCEPTION 'Le projet % (ID: %) a une version (%), inférieure à 8.23.0.', project_name, project_id, last_version; + END IF; + END LOOP; +END $$; diff --git a/apps/server-nestjs/src/prisma/migrations/20250121222954_drop_organization/migration.sql b/apps/server-nestjs/src/prisma/migrations/20250121222954_drop_organization/migration.sql new file mode 100644 index 000000000..54871c901 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20250121222954_drop_organization/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - You are about to drop the column `organizationId` on the `Project` table. All the data in the column will be lost. + - You are about to drop the `Organization` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Project" DROP CONSTRAINT "Project_organizationId_fkey"; + +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "organizationId"; + +-- DropTable +DROP TABLE "Organization"; diff --git a/apps/server-nestjs/src/prisma/migrations/20250723141246_dso/migration.sql b/apps/server-nestjs/src/prisma/migrations/20250723141246_dso/migration.sql new file mode 100644 index 000000000..68ca0df2f --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20250723141246_dso/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Cluster" ALTER COLUMN "infos" SET DATA TYPE VARCHAR(1000); diff --git a/apps/server-nestjs/src/prisma/migrations/20250818095032_remove_quota/migration.sql b/apps/server-nestjs/src/prisma/migrations/20250818095032_remove_quota/migration.sql new file mode 100644 index 000000000..8364090d8 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20250818095032_remove_quota/migration.sql @@ -0,0 +1,44 @@ +-- AlterTable +ALTER TABLE "Environment" +ADD COLUMN "cpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "gpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "memory" REAL NOT NULL DEFAULT 0; + +COMMENT ON COLUMN "Environment".cpu IS 'CPU share as float (1 and 0.01 are valid values)'; +COMMENT ON COLUMN "Environment".gpu IS 'GPU share as float (1 and 0.01 are valid values)'; +COMMENT ON COLUMN "Environment".memory IS 'Memory value as GigaBytes (1 and 0.01 are valid values)'; + +-- Use values from Quota. Memory is an extract of q.memory numeric value as it contains a unit (e.g. '2Gi'). +UPDATE "Environment" +SET cpu = q.cpu, memory = COALESCE(NULLIF(regexp_replace(q.memory, '\D', '','g'), ''), '0')::numeric +FROM "Quota" q +WHERE "quotaId" = q."id"; + +/* + Warnings: + + - You are about to drop the column `quotaId` on the `Environment` table. All the data in the column will be lost. + - You are about to drop the `Quota` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_QuotaToStage` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Environment" DROP CONSTRAINT "Environment_quotaId_fkey"; + +-- DropForeignKey +ALTER TABLE "_QuotaToStage" DROP CONSTRAINT "_QuotaToStage_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_QuotaToStage" DROP CONSTRAINT "_QuotaToStage_B_fkey"; + +-- AlterTable +ALTER TABLE "Environment" DROP COLUMN "quotaId", +ALTER COLUMN "cpu" DROP DEFAULT, +ALTER COLUMN "gpu" DROP DEFAULT, +ALTER COLUMN "memory" DROP DEFAULT; + +-- DropTable +DROP TABLE "Quota"; + +-- DropTable +DROP TABLE "_QuotaToStage"; diff --git a/apps/server-nestjs/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql b/apps/server-nestjs/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql new file mode 100644 index 000000000..77f32b5ab --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Cluster" +ADD COLUMN "cpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "gpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "memory" REAL NOT NULL DEFAULT 0; diff --git a/apps/server-nestjs/src/prisma/migrations/20250916134454_add_project_resources/migration.sql b/apps/server-nestjs/src/prisma/migrations/20250916134454_add_project_resources/migration.sql new file mode 100644 index 000000000..decca804a --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20250916134454_add_project_resources/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "Project" +ADD COLUMN "limitless" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "hprodCpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "hprodGpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "hprodMemory" REAL NOT NULL DEFAULT 0, +ADD COLUMN "prodCpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "prodGpu" REAL NOT NULL DEFAULT 0, +ADD COLUMN "prodMemory" REAL NOT NULL DEFAULT 0; diff --git a/apps/server-nestjs/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql b/apps/server-nestjs/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql new file mode 100644 index 000000000..95f3a689d --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql @@ -0,0 +1,4 @@ +-- Rename default zone +UPDATE "Zone" +SET ("label", "description") = ('DSO', 'Zone par défaut') +WHERE slug = 'default'; diff --git a/apps/server-nestjs/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql b/apps/server-nestjs/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql new file mode 100644 index 000000000..aadb6cdba --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Repository" ADD COLUMN "deployRevision" TEXT, +ADD COLUMN "deployPath" TEXT, +ADD COLUMN "helmValuesFiles" TEXT; diff --git a/apps/server-nestjs/src/prisma/migrations/migration_lock.toml b/apps/server-nestjs/src/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..648c57fd5 --- /dev/null +++ b/apps/server-nestjs/src/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" \ No newline at end of file diff --git a/apps/server-nestjs/src/prisma/schema/admin.prisma b/apps/server-nestjs/src/prisma/schema/admin.prisma new file mode 100644 index 000000000..71cfb1754 --- /dev/null +++ b/apps/server-nestjs/src/prisma/schema/admin.prisma @@ -0,0 +1,20 @@ +model AdminPlugin { + pluginName String + key String + value String + + @@unique([pluginName, key]) +} + +model AdminRole { + id String @id @unique @default(uuid()) @db.Uuid + name String + permissions BigInt + position Int @db.SmallInt + oidcGroup String @default("") +} + +model SystemSetting { + key String @id @unique + value String +} diff --git a/apps/server-nestjs/src/prisma/schema/project.prisma b/apps/server-nestjs/src/prisma/schema/project.prisma new file mode 100644 index 000000000..e76048675 --- /dev/null +++ b/apps/server-nestjs/src/prisma/schema/project.prisma @@ -0,0 +1,106 @@ +model Environment { + id String @id @default(uuid()) @db.Uuid + name String @db.VarChar(11) + projectId String @db.Uuid + memory Float @db.Real + cpu Float @db.Real + gpu Float @db.Real + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + clusterId String @db.Uuid + stageId String @db.Uuid + cluster Cluster @relation(fields: [clusterId], references: [id]) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + stage Stage @relation(fields: [stageId], references: [id]) + + @@unique([projectId, name]) +} + +model Repository { + id String @id @default(uuid()) @db.Uuid + projectId String @db.Uuid + internalRepoName String + externalRepoUrl String @default("") + externalUserName String @default("") + isInfra Boolean @default(false) + isPrivate Boolean @default(false) + deployRevision String @default("") + deployPath String @default("") + helmValuesFiles String @default("") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) +} + +model ProjectClusterHistory { + projectId String @db.Uuid + clusterId String @db.Uuid + + @@unique([projectId, clusterId]) +} + +model ProjectMembers { + projectId String @db.Uuid + userId String @db.Uuid + roleIds String[] + project Project @relation(fields: [projectId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@unique([projectId, userId]) +} + +model ProjectPlugin { + pluginName String + projectId String @db.Uuid + key String + value String + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@unique([projectId, pluginName, key]) +} + +model ProjectRole { + id String @id @unique @default(uuid()) @db.Uuid + name String + permissions BigInt + projectId String @db.Uuid + position Int @db.SmallInt + project Project @relation(fields: [projectId], references: [id]) +} + +model Project { + id String @id @unique @default(uuid()) @db.Uuid + name String + description String @default("") + status ProjectStatus @default(initializing) + locked Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + everyonePerms BigInt @default(896) + ownerId String @db.Uuid + environments Environment[] + logs Log[] + owner User @relation(fields: [ownerId], references: [id]) + members ProjectMembers[] + plugins ProjectPlugin[] + roles ProjectRole[] + repositories Repository[] + clusters Cluster[] @relation("ClusterToProject") + slug String @unique + limitless Boolean @default(true) + hprodCpu Float @db.Real + hprodGpu Float @db.Real + hprodMemory Float @db.Real + prodCpu Float @db.Real + prodGpu Float @db.Real + prodMemory Float @db.Real + lastSuccessProvisionningVersion String? +} + +enum ProjectStatus { + initializing + created + failed + archived + warning +} diff --git a/apps/server-nestjs/src/prisma/schema/schema.prisma b/apps/server-nestjs/src/prisma/schema/schema.prisma new file mode 100644 index 000000000..aadf7fea1 --- /dev/null +++ b/apps/server-nestjs/src/prisma/schema/schema.prisma @@ -0,0 +1,21 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DB_URL") +} + +model Log { + id String @id @default(uuid()) @db.Uuid + data Json + action String @default("") + userId String? @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + requestId String? @db.VarChar(36) + projectId String? @db.Uuid + project Project? @relation(fields: [projectId], references: [id]) + user User? @relation(fields: [userId], references: [id]) +} diff --git a/apps/server-nestjs/src/prisma/schema/token.prisma b/apps/server-nestjs/src/prisma/schema/token.prisma new file mode 100644 index 000000000..c0c55751c --- /dev/null +++ b/apps/server-nestjs/src/prisma/schema/token.prisma @@ -0,0 +1,30 @@ +model AdminToken { + id String @id @unique @default(uuid()) @db.Uuid + name String + permissions BigInt + userId String @db.Uuid + expirationDate DateTime? + lastUse DateTime? + createdAt DateTime @default(now()) + status TokenStatus @default(active) + hash String + owner User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) +} + +model PersonalAccessToken { + id String @id @unique @default(uuid()) @db.Uuid + name String + userId String @db.Uuid + expirationDate DateTime + lastUse DateTime? + createdAt DateTime @default(now()) + status TokenStatus @default(active) + hash String + owner User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) +} + +enum TokenStatus { + active + revoked + inactive +} diff --git a/apps/server-nestjs/src/prisma/schema/topography.prisma b/apps/server-nestjs/src/prisma/schema/topography.prisma new file mode 100644 index 000000000..ad8e3be22 --- /dev/null +++ b/apps/server-nestjs/src/prisma/schema/topography.prisma @@ -0,0 +1,53 @@ +model Cluster { + id String @id @unique @default(uuid()) @db.Uuid + label String @unique @db.VarChar(50) + privacy ClusterPrivacy @default(dedicated) + secretName String @unique @default(uuid()) @db.VarChar(50) + clusterResources Boolean @default(false) + kubeConfigId String @unique @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + infos String? @db.VarChar(1000) + external Boolean @default(false) + memory Float @db.Real + cpu Float @db.Real + gpu Float @db.Real + zoneId String @db.Uuid + kubeconfig Kubeconfig @relation(fields: [kubeConfigId], references: [id], onDelete: Cascade) + zone Zone @relation(fields: [zoneId], references: [id]) + environments Environment[] + projects Project[] @relation("ClusterToProject") + stages Stage[] @relation("ClusterToStage") +} + +model Kubeconfig { + id String @id @unique @default(uuid()) @db.Uuid + user Json + cluster Json + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + parentCluster Cluster? +} + +model Stage { + id String @id @unique @default(uuid()) @db.Uuid + name String @unique @db.VarChar + environments Environment[] + clusters Cluster[] @relation("ClusterToStage") +} + +model Zone { + id String @id @unique @default(uuid()) @db.Uuid + slug String @unique @db.VarChar(10) + label String @db.VarChar(50) + description String? @db.VarChar(200) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + argocdUrl String @default("https://example.com") + clusters Cluster[] +} + +enum ClusterPrivacy { + public + dedicated +} diff --git a/apps/server-nestjs/src/prisma/schema/user.prisma b/apps/server-nestjs/src/prisma/schema/user.prisma new file mode 100644 index 000000000..e90fb69f8 --- /dev/null +++ b/apps/server-nestjs/src/prisma/schema/user.prisma @@ -0,0 +1,23 @@ +model User { + id String @id @db.Uuid + firstName String + lastName String + email String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastLogin DateTime? + adminRoleIds String[] + type UserType + + logs Log[] + projectsOwned Project[] + adminTokens AdminToken[] + projectMembers ProjectMembers[] + personalAccessTokens PersonalAccessToken[] +} + +enum UserType { + human + bot + ghost +} From 28d4ff532ebbb3a4dc23f5acc2f41262c15df9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 15 Dec 2025 15:35:18 +0100 Subject: [PATCH 20/33] chore(server-nestjs): rework configuration service to properly instanciate logger service --- apps/server-nestjs/.env-example | 18 ++ apps/server-nestjs/.env.docker-example | 13 ++ apps/server-nestjs/.env.integ-example | 43 +++++ apps/server-nestjs/README.md | 4 +- apps/server-nestjs/package.json | 1 + .../configuration/configuration.module.ts | 18 +- .../configuration/configuration.service.ts | 48 +++++- .../infrastructure/logger/logger.module.ts | 9 +- .../old-server/src/init/db/index.ts | 94 +++++----- .../cpin-module/old-server/src/utils/env.ts | 114 ++++++------ .../old-server/src/utils/logger.ts | 162 +++++++++--------- apps/server-nestjs/tsconfig.json | 51 +++--- pnpm-lock.yaml | 31 +++- 13 files changed, 389 insertions(+), 217 deletions(-) create mode 100644 apps/server-nestjs/.env-example create mode 100644 apps/server-nestjs/.env.docker-example create mode 100644 apps/server-nestjs/.env.integ-example diff --git a/apps/server-nestjs/.env-example b/apps/server-nestjs/.env-example new file mode 100644 index 000000000..84236efbe --- /dev/null +++ b/apps/server-nestjs/.env-example @@ -0,0 +1,18 @@ +DEV_SETUP="true" +NODE_ENV=development +# HOME=/home/node +SESSION_SECRET=a-very-strong-secret-with-more-than-32-char +KEYCLOAK_DOMAIN=localhost:8090 +KEYCLOAK_REALM=cloud-pi-native +KEYCLOAK_PROTOCOL=http +KEYCLOAK_CLIENT_ID=dso-console-backend +KEYCLOAK_CLIENT_SECRET=client-secret-backend +KEYCLOAK_REDIRECT_URI=http://localhost:8080 +SERVER_PORT=4000 +DB_URL=postgresql://admin:admin@localhost:5432/dso-console-db?schema=public +CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr + +# Configuration OpenCDS +OPENCDS_URL= +OPENCDS_API_TOKEN=token +OPENCDS_API_TLS_REJECT_UNAUTHORIZED=true diff --git a/apps/server-nestjs/.env.docker-example b/apps/server-nestjs/.env.docker-example new file mode 100644 index 000000000..0da54a8e4 --- /dev/null +++ b/apps/server-nestjs/.env.docker-example @@ -0,0 +1,13 @@ +DOCKER=true +DEV_SETUP="true" +NODE_ENV=development +SESSION_SECRET=a-very-strong-secret-with-more-than-32-char +KEYCLOAK_DOMAIN=keycloak:8080 +KEYCLOAK_REALM=cloud-pi-native +KEYCLOAK_PROTOCOL=http +KEYCLOAK_CLIENT_ID=dso-console-backend +KEYCLOAK_CLIENT_SECRET=client-secret-backend +KEYCLOAK_REDIRECT_URI=http://localhost:8080 +SERVER_PORT=8080 +DB_URL=postgresql://admin:admin@postgres:5432/dso-console-db?schema=public +CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr diff --git a/apps/server-nestjs/.env.integ-example b/apps/server-nestjs/.env.integ-example new file mode 100644 index 000000000..33e23e778 --- /dev/null +++ b/apps/server-nestjs/.env.integ-example @@ -0,0 +1,43 @@ +DEV_SETUP="false" +INTEGRATION=true +KEYCLOAK_PROTOCOL=https +KEYCLOAK_CLIENT_ID= +KEYCLOAK_CLIENT_SECRET= +KEYCLOAK_DOMAIN= +KEYCLOAK_REALM= +ARGO_NAMESPACE= +ARGOCD_URL= +GITLAB_TOKEN= +GITLAB_URL= +HARBOR_ADMIN= +HARBOR_ADMIN_PASSWORD= +HARBOR_URL= +KEYCLOAK_ADMIN= +KEYCLOAK_ADMIN_PASSWORD= +KEYCLOAK_URL= +NEXUS_ADMIN= +NEXUS_ADMIN_PASSWORD= +NEXUS_URL= +PROJECTS_ROOT_DIR= +SONAR_API_TOKEN= +SONARQUBE_URL= +VAULT_TOKEN= +VAULT_URL= + +KUBECONFIG_HOST_PATH= +KUBECONFIG_PATH=$HOME/.kube/config +KUBECONFIG_CTX= + +EXTERNAL_PLUGINS_DIR_HOST_PATH=/path/to/plugins + +# Variables de plugins externes + +# GRAVITEE_APIM_API_ID= +# GRAVITEE_APIM_PLAN_ID= +# GRAVITEE_APIM_TOKEN= +# GRAVITEE_APIM_URL= +# GRAVITEE_GATEWAY_URL= + +# HTTP_PROXY= +# HTTPS_PROXY= +# NO_PROXY= diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md index bc9c72411..94856fe50 100644 --- a/apps/server-nestjs/README.md +++ b/apps/server-nestjs/README.md @@ -240,5 +240,7 @@ server-nestjs/$ find src/cpin-module/old-server -type f -iname "*.ts" -exec sed ## To delete (once we have a sastifying nestjs implementation): ``` -old-server/src/utils/logger.ts +old-server/src/utils/logger.ts -> Replaced by LoggerModule +old-server/src/utils/env.ts -> Replaced by ConfigurationModule +old-server/src/init/db/* (except dump.ts) -> Replaced by DatabaseInitializationService ``` diff --git a/apps/server-nestjs/package.json b/apps/server-nestjs/package.json index 146176fa5..bd0ae1726 100644 --- a/apps/server-nestjs/package.json +++ b/apps/server-nestjs/package.json @@ -35,6 +35,7 @@ "@kubernetes-models/argo-cd": "^2.6.2", "@kubernetes/client-node": "^0.22.3", "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@prisma/client": "^6.0.1", diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.module.ts index c68567a33..b2156ef75 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.module.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.module.ts @@ -1,9 +1,25 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { ConfigurationService } from './configuration.service'; +const pathList: string[] = []; + +if (process.env.DOCKER !== 'true') { + pathList.push('.env'); +} + +if (process.env.INTEGRATION === 'true') { + pathList.push('.env.integ'); +} + @Module({ + imports: [ + ConfigModule.forRoot({ + envFilePath: pathList, + }), + ], providers: [ConfigurationService], - exports: [ConfigurationService] + exports: [ConfigurationService], }) export class ConfigurationModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts index b0a1e4f9b..b0a10de4c 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts @@ -2,6 +2,50 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class ConfigurationService { - // @TODO: Rework this with proper environment handling - public readonly environment = 'development'; + // application mode + isDev = process.env.NODE_ENV === 'development'; + isTest = process.env.NODE_ENV === 'test'; + isProd = process.env.NODE_ENV === 'production'; + isInt = process.env.INTEGRATION === 'true'; + isCI = process.env.CI === 'true'; + isDevSetup = process.env.DEV_SETUP === 'true'; + + // app + port = process.env.SERVER_PORT; + appVersion = this.isProd ? (process.env.APP_VERSION ?? 'unknown') : 'dev'; + + // db + dbUrl = process.env.DB_URL; + + // keycloak + sessionSecret = process.env.SESSION_SECRET; + keycloakProtocol = process.env.KEYCLOAK_PROTOCOL; + keycloakDomain = process.env.KEYCLOAK_DOMAIN; + keycloakRealm = process.env.KEYCLOAK_REALM; + keycloakClientId = process.env.KEYCLOAK_CLIENT_ID; + keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET; + keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI; + adminsUserId = process.env.ADMIN_KC_USER_ID + ? process.env.ADMIN_KC_USER_ID.split(',') + : []; + + contactEmail = + process.env.CONTACT_EMAIL ?? + 'cloudpinative-relations@interieur.gouv.fr'; + + // plugins + mockPlugins = process.env.MOCK_PLUGINS === 'true'; + projectRootDir = process.env.PROJECTS_ROOT_DIR; + pluginsDir = process.env.PLUGINS_DIR ?? '/plugins'; + NODE_ENV = + process.env.NODE_ENV === 'test' + ? 'test' + : process.env.NODE_ENV === 'development' + ? 'development' + : 'production'; + + // server tuning + parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT + ? Number(process.env.PARALLEL_BULK_LIMIT) + : 5; } diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts index 1c0ee6a4d..ba71fe99d 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts @@ -39,7 +39,14 @@ export const loggerConfiguration: Record = { inject: [ConfigurationService], useFactory: async (configService: ConfigurationService) => { return { - pinoHttp: loggerConfiguration[configService.environment], + pinoHttp: + loggerConfiguration[ + configService.isProd + ? 'production' + : configService.isTest + ? 'test' + : 'development' + ], }; }, }), diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts index edb03b728..cc1d8f04b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts @@ -1,51 +1,51 @@ -import { modelKeys } from './utils' -import { logger } from '@old-server/app' -import prisma from '@old-server/prisma' +// import { modelKeys } from './utils' +// import { logger } from '@old-server/app' +// import prisma from '@old-server/prisma' -type ExtractKeysWithFields = { - [K in keyof T]: T[K] extends { fields: any } ? K : never -}[keyof T] +// type ExtractKeysWithFields = { + // [K in keyof T]: T[K] extends { fields: any } ? K : never +// }[keyof T] -type Models = ExtractKeysWithFields +// type Models = ExtractKeysWithFields -type Imports = Partial> & { - associations: [Models, any[]] -} +// type Imports = Partial> & { + // associations: [Models, any[]] +// } -export async function initDb(data: Imports) { - const dataStringified = JSON.stringify(data) - const dataParsed = JSON.parse(dataStringified, (key, value) => { - try { - if (['permissions', 'everyonePerms'].includes(key)) { - return BigInt(value.slice(0, value.length - 1)) - } - } catch (_error) { - return value - } - return value - }) - logger.info('Drop tables') - for (const modelKey of modelKeys.toReversed()) { - // @ts-ignore - await prisma[modelKey].deleteMany() - } - logger.info('Import models') - for (const modelKey of modelKeys) { - // @ts-ignore - await prisma[modelKey].createMany({ data: dataParsed[modelKey] }) - } - logger.info('Import associations') - for (const [modelKey, rows] of dataParsed.associations) { - for (const row of rows) { - const idKey = 'id' - const connectKeys = Object.keys(row).filter(key => key !== idKey) - const dataConnects = connectKeys.reduce((acc, curr) => { - acc[curr] = { connect: row[curr] } - return acc - }, {} as Record) - // @ts-ignore - await prisma[modelKey].update({ where: { id: row.id }, data: dataConnects }) - } - } - logger.info('End import') -} +// export async function initDb(data: Imports) { + // const dataStringified = JSON.stringify(data) + // const dataParsed = JSON.parse(dataStringified, (key, value) => { + // try { + // if (['permissions', 'everyonePerms'].includes(key)) { + // return BigInt(value.slice(0, value.length - 1)) + // } + // } catch (_error) { + // return value + // } + // return value + // }) + // logger.info('Drop tables') + // for (const modelKey of modelKeys.toReversed()) { + // // @ts-ignore + // await prisma[modelKey].deleteMany() + // } + // logger.info('Import models') + // for (const modelKey of modelKeys) { + // // @ts-ignore + // await prisma[modelKey].createMany({ data: dataParsed[modelKey] }) + // } + // logger.info('Import associations') + // for (const [modelKey, rows] of dataParsed.associations) { + // for (const row of rows) { + // const idKey = 'id' + // const connectKeys = Object.keys(row).filter(key => key !== idKey) + // const dataConnects = connectKeys.reduce((acc, curr) => { + // acc[curr] = { connect: row[curr] } + // return acc + // }, {} as Record) + // // @ts-ignore + // await prisma[modelKey].update({ where: { id: row.id }, data: dataConnects }) + // } + // } + // logger.info('End import') +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts index fc41aab75..1a5caf387 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts @@ -1,57 +1,57 @@ -import * as dotenv from 'dotenv' - -if (process.env.DOCKER !== 'true') { - dotenv.config({ path: '.env' }) -} - -if (process.env.INTEGRATION === 'true') { - const envInteg = dotenv.config({ path: '.env.integ' }) - process.env = { - ...process.env, - ...(envInteg?.parsed ?? {}), - } -} - -// application mode -export const isDev = process.env.NODE_ENV === 'development' -export const isTest = process.env.NODE_ENV === 'test' -export const isProd = process.env.NODE_ENV === 'production' -export const isInt = process.env.INTEGRATION === 'true' -export const isCI = process.env.CI === 'true' -export const isDevSetup = process.env.DEV_SETUP === 'true' - -// app -export const port = process.env.SERVER_PORT -export const appVersion = isProd - ? (process.env.APP_VERSION ?? 'unknown') - : 'dev' - -// db -export const dbUrl = process.env.DB_URL - -// keycloak -export const sessionSecret = process.env.SESSION_SECRET -export const keycloakProtocol = process.env.KEYCLOAK_PROTOCOL -export const keycloakDomain = process.env.KEYCLOAK_DOMAIN -export const keycloakRealm = process.env.KEYCLOAK_REALM -export const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID -export const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET -export const keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI -export const adminsUserId = process.env.ADMIN_KC_USER_ID - ? process.env.ADMIN_KC_USER_ID.split(',') - : [] - -export const contactEmail = process.env.CONTACT_EMAIL ?? 'cloudpinative-relations@interieur.gouv.fr' - -// plugins -export const mockPlugins = process.env.MOCK_PLUGINS === 'true' -export const projectRootDir = process.env.PROJECTS_ROOT_DIR -export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins' -export const NODE_ENV = process.env.NODE_ENV === 'test' - ? 'test' - : process.env.NODE_ENV === 'development' - ? 'development' - : 'production' - -// server tuning -export const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT ? Number(process.env.PARALLEL_BULK_LIMIT) : 5 +// import * as dotenv from 'dotenv' + +// if (process.env.DOCKER !== 'true') { + // dotenv.config({ path: '.env' }) +// } + +// if (process.env.INTEGRATION === 'true') { + // const envInteg = dotenv.config({ path: '.env.integ' }) + // process.env = { + // ...process.env, + // ...(envInteg?.parsed ?? {}), + // } +// } + +// // application mode +// export const isDev = process.env.NODE_ENV === 'development' +// export const isTest = process.env.NODE_ENV === 'test' +// export const isProd = process.env.NODE_ENV === 'production' +// export const isInt = process.env.INTEGRATION === 'true' +// export const isCI = process.env.CI === 'true' +// export const isDevSetup = process.env.DEV_SETUP === 'true' + +// // app +// export const port = process.env.SERVER_PORT +// export const appVersion = isProd + // ? (process.env.APP_VERSION ?? 'unknown') + // : 'dev' + +// // db +// export const dbUrl = process.env.DB_URL + +// // keycloak +// export const sessionSecret = process.env.SESSION_SECRET +// export const keycloakProtocol = process.env.KEYCLOAK_PROTOCOL +// export const keycloakDomain = process.env.KEYCLOAK_DOMAIN +// export const keycloakRealm = process.env.KEYCLOAK_REALM +// export const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID +// export const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET +// export const keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI +// export const adminsUserId = process.env.ADMIN_KC_USER_ID + // ? process.env.ADMIN_KC_USER_ID.split(',') + // : [] + +// export const contactEmail = process.env.CONTACT_EMAIL ?? 'cloudpinative-relations@interieur.gouv.fr' + +// // plugins +// export const mockPlugins = process.env.MOCK_PLUGINS === 'true' +// export const projectRootDir = process.env.PROJECTS_ROOT_DIR +// export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins' +// export const NODE_ENV = process.env.NODE_ENV === 'test' + // ? 'test' + // : process.env.NODE_ENV === 'development' + // ? 'development' + // : 'production' + +// // server tuning +// export const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT ? Number(process.env.PARALLEL_BULK_LIMIT) : 5 diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts index 5e5ebd1b6..f6ded45b5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts @@ -1,89 +1,89 @@ -import type { FastifyBaseLogger, FastifyLogFn, PinoLoggerOptions } from 'fastify/types/logger' -import type { XOR } from '@cpn-console/shared' -import { logger as customLogger } from '@old-server/app' +// import type { FastifyBaseLogger, FastifyLogFn, PinoLoggerOptions } from 'fastify/types/logger' +// import type { XOR } from '@cpn-console/shared' +// import { logger as customLogger } from '@old-server/app' -export const customLevels = { - audit: 25, -} +// export const customLevels = { + // audit: 25, +// } -export const loggerConf: Record = { - development: { - transport: { - target: 'pino-pretty', - options: { - translateTime: 'dd/mm/yyyy - HH:MM:ss Z', - ignore: 'pid,hostname', - colorize: true, - singleLine: true, - }, - }, - customLevels, - level: process.env.LOG_LEVEL ?? 'debug', - }, - production: { - customLevels, - level: process.env.LOG_LEVEL ?? 'audit', - }, - test: { - level: 'silent', - }, -} +// export const loggerConf: Record = { + // development: { + // transport: { + // target: 'pino-pretty', + // options: { + // translateTime: 'dd/mm/yyyy - HH:MM:ss Z', + // ignore: 'pid,hostname', + // colorize: true, + // singleLine: true, + // }, + // }, + // customLevels, + // level: process.env.LOG_LEVEL ?? 'debug', + // }, + // production: { + // customLevels, + // level: process.env.LOG_LEVEL ?? 'audit', + // }, + // test: { + // level: 'silent', + // }, +// } -type LoggerType = 'info' | 'warn' | 'error' | 'fatal' | 'trace' | 'debug' | 'audit' | undefined -const loggerWrapper = { - level: '', - child: () => loggerWrapper, - silent: () => {}, - audit: (msg: string | unknown) => console.log(msg), - info: (msg: string | unknown) => console.log(msg), - warn: (msg: string | unknown) => console.warn(msg), - error: (msg: string | unknown) => console.error(msg), - fatal: (msg: string | unknown) => console.error(msg), - trace: (msg: string | unknown) => console.trace(msg), - debug: (msg: string | unknown) => console.debug(msg), -} +// type LoggerType = 'info' | 'warn' | 'error' | 'fatal' | 'trace' | 'debug' | 'audit' | undefined +// const loggerWrapper = { + // level: '', + // child: () => loggerWrapper, + // silent: () => {}, + // audit: (msg: string | unknown) => console.log(msg), + // info: (msg: string | unknown) => console.log(msg), + // warn: (msg: string | unknown) => console.warn(msg), + // error: (msg: string | unknown) => console.error(msg), + // fatal: (msg: string | unknown) => console.error(msg), + // trace: (msg: string | unknown) => console.trace(msg), + // debug: (msg: string | unknown) => console.debug(msg), +// } -export function log( - type: LoggerType, - { - reqId, - userId, - tokenId, - message, - error, - infos, - }: { - reqId?: string - userId?: string - tokenId?: string - infos?: Record - } & XOR<{ message: string }, { error: Record | string | Error }>, -) { - const logger = customLogger || loggerWrapper +// export function log( + // type: LoggerType, + // { + // reqId, + // userId, + // tokenId, + // message, + // error, + // infos, + // }: { + // reqId?: string + // userId?: string + // tokenId?: string + // infos?: Record + // } & XOR<{ message: string }, { error: Record | string | Error }>, +// ) { + // const logger = customLogger || loggerWrapper - const logInfos = { - message, - infos, - reqId, - userId, - tokenId, - } + // const logInfos = { + // message, + // infos, + // reqId, + // userId, + // tokenId, + // } - if (error) { - const errorInfos = { - ...logInfos, - error: { - message: typeof error === 'string' ? error : error?.message || 'unexpected error', - trace: error instanceof Error && error?.stack, - }, - } - logger.error({ ...errorInfos }) - return - } - logger[type || 'info']({ reqId, userId, logInfos }) -} + // if (error) { + // const errorInfos = { + // ...logInfos, + // error: { + // message: typeof error === 'string' ? error : error?.message || 'unexpected error', + // trace: error instanceof Error && error?.stack, + // }, + // } + // logger.error({ ...errorInfos }) + // return + // } + // logger[type || 'info']({ reqId, userId, logInfos }) +// } -export interface CustomLogger extends FastifyBaseLogger { +// export interface CustomLogger extends FastifyBaseLogger { /** * Log at `'audit'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. * If more args follows `msg`, these will be used to format `msg` using `util.format`. @@ -93,5 +93,5 @@ export interface CustomLogger extends FastifyBaseLogger { * @param msg: the log message to write * @param ...args: format string values when `msg` is a format string */ - audit: FastifyLogFn -} + // audit: FastifyLogFn +// } diff --git a/apps/server-nestjs/tsconfig.json b/apps/server-nestjs/tsconfig.json index fc17a4bf6..33025606b 100644 --- a/apps/server-nestjs/tsconfig.json +++ b/apps/server-nestjs/tsconfig.json @@ -1,26 +1,29 @@ { - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "baseUrl": "./", - "declaration": true, - "emitDecoratorMetadata": true, - "esModuleInterop": true, - "experimentalDecorators": true, - "forceConsistentCasingInFileNames": true, - "incremental": true, - "isolatedModules": true, - "module": "nodenext", - "moduleResolution": "nodenext", - "noFallthroughCasesInSwitch": false, - "noImplicitAny": false, - "outDir": "./dist", - "paths": { "@old-server/*": ["src/cpin-module/old-server/src/*"] }, - "removeComments": true, - "resolvePackageJsonExports": true, - "skipLibCheck": true, - "sourceMap": true, - "strictBindCallApply": false, - "strictNullChecks": true, - "target": "ES2023" - } + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "baseUrl": "./", + "declaration": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "isolatedModules": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "noFallthroughCasesInSwitch": false, + "noImplicitAny": false, + "outDir": "./dist", + "paths": { + "@/*": ["src/*"], + "@old-server/*": ["src/cpin-module/old-server/src/*"] + }, + "removeComments": true, + "resolvePackageJsonExports": true, + "skipLibCheck": true, + "sourceMap": true, + "strictBindCallApply": false, + "strictNullChecks": true, + "target": "ES2023" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8c3c1fe2..0617cc818 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,6 +428,9 @@ importers: '@nestjs/common': specifier: ^11.0.1 version: 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.2 + version: 4.0.2(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.1 version: 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -469,7 +472,7 @@ importers: version: 4.2.0 nestjs-pino: specifier: ^4.5.0 - version: 4.5.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) + version: 4.5.0(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) pino-http: specifier: ^11.0.0 version: 11.0.0 @@ -2939,6 +2942,12 @@ packages: class-validator: optional: true + '@nestjs/config@4.0.2': + resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + '@nestjs/core@11.1.11': resolution: {integrity: sha512-H9i+zT3RvHi7tDc+lCmWHJ3ustXveABCr+Vcpl96dNOxgmrx4elQSTC4W93Mlav2opfLV+p0UTHY6L+bpUA4zA==} engines: {node: '>= 20'} @@ -4806,6 +4815,10 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dotenv-expand@12.0.1: + resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==} + engines: {node: '>=12'} + dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} @@ -11408,6 +11421,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/config@4.0.2(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 16.4.7 + dotenv-expand: 12.0.1 + lodash: 4.17.21 + rxjs: 7.8.2 + '@nestjs/core@11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -13489,6 +13510,10 @@ snapshots: dependencies: is-obj: 2.0.0 + dotenv-expand@12.0.1: + dependencies: + dotenv: 16.6.1 + dotenv@16.4.7: {} dotenv@16.6.1: {} @@ -16332,9 +16357,9 @@ snapshots: neo-async@2.6.2: {} - nestjs-pino@4.5.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): + nestjs-pino@4.5.0(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2) pino: 10.1.0 pino-http: 11.0.0 rxjs: 7.8.2 From 6ae6d5a1132620b5da4a7714672330777c778722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 15 Dec 2025 15:39:00 +0100 Subject: [PATCH 21/33] chore(server-nestjs): add database initialization code in DatabaseInitializationService --- .../application-initialization.service.ts | 125 +++++++++++++++++- .../application-initialization.module.ts | 2 + .../database-initialization.service.ts | 67 +++++++++- .../database-initialization/utils.spec.ts | 52 ++++++++ .../database-initialization/utils.ts | 85 ++++++++++++ 5 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.ts diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts index 308131740..f34ff379d 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts @@ -1,4 +1,125 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; +import { rm } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { ConfigurationService } from 'src/cpin-module/infrastructure/configuration/configuration.service'; + +import app from './app'; +import { getConnection } from './connect'; +import { initDb } from './init/db/index'; +import { initPm } from './plugins'; @Injectable() -export class ApplicationInitializationService {} +export class ApplicationInitializationService { + private readonly logger = new Logger(ApplicationInitializationService.name); + constructor(private readonly config: ConfigurationService) {} + + async setUpHTTPProxy() { + // Workaround because fetch isn't using http_proxy variables + // See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 + if (process.env.HTTP_PROXY) { + const Undici = await import('undici'); + const ProxyAgent = Undici.ProxyAgent; + const setGlobalDispatcher = Undici.setGlobalDispatcher; + setGlobalDispatcher(new ProxyAgent(process.env.HTTP_PROXY)); + } + } + + async injectDataInDatabase(path: string) { + this.logger.log('Starting init DB...'); + const { data } = await import(path); + await initDb(data); + this.logger.log('initDb invoked successfully'); + } + + async startServer() { + try { + await getConnection(); + } catch (error) { + if (!(error instanceof Error)) return; + this.logger.error(error.message); + throw error; + } + + initPm(); + + this.logger.log('Reading init database file'); + + try { + const dataPath = + this.config.isProd || this.config.isInt + ? './init/db/imports/data' + : '@cpn-console/test-utils/src/imports/data'; + await this.injectDataInDatabase(dataPath); + if (this.config.isProd && !this.config.isDevSetup) { + this.logger.log('Cleaning up imported data file...'); + await rm(resolve(__dirname, dataPath)); + this.logger.log(`Successfully deleted '${dataPath}'`); + } + } catch (error) { + if ( + error.code === 'ERR_MODULE_NOT_FOUND' || + error.message.includes('Failed to load') || + error.message.includes('Cannot find module') + ) { + this.logger.log('No initDb file, skipping'); + } else { + this.logger.warn(error.message); + throw error; + } + } + + this.logger.debug({ + isDev: this.config.isDev, + isTest: this.config.isTest, + isCI: this.config.isCI, + isDevSetup: this.config.isDevSetup, + isProd: this.config.isProd, + }); + } + + async getPreparedApp() { + try { + await getConnection(); + } catch (error) { + this.logger.error(error.message); + throw error; + } + + initPm(); + + this.logger.log('Reading init database file'); + + try { + const dataPath = + this.config.isProd || this.config.isInt + ? './init/db/imports/data' + : '@cpn-console/test-utils/src/imports/data'; + await this.injectDataInDatabase(dataPath); + if (this.config.isProd && !this.config.isDevSetup) { + this.logger.log('Cleaning up imported data file...'); + await rm(resolve(__dirname, dataPath)); + this.logger.log(`Successfully deleted '${dataPath}'`); + } + } catch (error) { + if ( + error.code === 'ERR_MODULE_NOT_FOUND' || + error.message.includes('Failed to load') || + error.message.includes('Cannot find module') + ) { + this.logger.log('No initDb file, skipping'); + } else { + this.logger.warn(error.message); + throw error; + } + } + + this.logger.debug({ + isDev: this.config.isDev, + isTest: this.config.isTest, + isCI: this.config.isCI, + isDevSetup: this.config.isDevSetup, + isProd: this.config.isProd, + }); + return app; + } +} diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts index b4dc622c7..f361ffc82 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; +import { ConfigurationModule } from '../infrastructure/configuration/configuration.module'; import { ApplicationInitializationService } from './application-initialization-service/application-initialization.service'; import { DatabaseInitializationService } from './database-initialization/database-initialization.service'; import { PluginManagementService } from './plugin-management/plugin-management.service'; @Module({ + imports: [ConfigurationModule], providers: [ ApplicationInitializationService, DatabaseInitializationService, diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts index 825e028ab..14d3152d6 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts @@ -1,4 +1,67 @@ -import { Injectable } from '@nestjs/common'; +import prisma from '@/prisma'; +import { Injectable, Logger } from '@nestjs/common'; + +import { modelKeys } from './utils'; + +type ExtractKeysWithFields = { + [K in keyof T]: T[K] extends { fields: any } ? K : never; +}[keyof T]; + +type Models = ExtractKeysWithFields; + +type Imports = Partial> & { + associations: [Models, any[]]; +}; @Injectable() -export class DatabaseInitializationService {} +export class DatabaseInitializationService { + private readonly loggerService = new Logger( + DatabaseInitializationService.name, + ); + + async initDb(data: Imports) { + const dataStringified = JSON.stringify(data); + const dataParsed = JSON.parse(dataStringified, (key, value) => { + try { + if (['permissions', 'everyonePerms'].includes(key)) { + return BigInt(value.slice(0, value.length - 1)); + } + } catch (_error) { + return value; + } + return value; + }); + this.loggerService.log('Drop tables'); + for (const modelKey of modelKeys.toReversed()) { + // @ts-ignore + await prisma[modelKey].deleteMany(); + } + this.loggerService.log('Import models'); + for (const modelKey of modelKeys) { + // @ts-ignore + await prisma[modelKey].createMany({ data: dataParsed[modelKey] }); + } + this.loggerService.log('Import associations'); + for (const [modelKey, rows] of dataParsed.associations) { + for (const row of rows) { + const idKey = 'id'; + const connectKeys = Object.keys(row).filter( + (key) => key !== idKey, + ); + const dataConnects = connectKeys.reduce( + (acc, curr) => { + acc[curr] = { connect: row[curr] }; + return acc; + }, + {} as Record, + ); + // @ts-ignore + await prisma[modelKey].update({ + where: { id: row.id }, + data: dataConnects, + }); + } + } + this.loggerService.log('End import'); + } +} diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.spec.ts b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.spec.ts new file mode 100644 index 000000000..4377e07a1 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.spec.ts @@ -0,0 +1,52 @@ +import { describe, expect, it, vi } from 'vitest' +import prisma from '../../__mocks__/prisma' +import { modelKeys, moveBefore, resourceListToDict } from './utils' + +vi.mock('fs', () => ({ writeFileSync: vi.fn() })) +for (const modelKey of modelKeys) { + prisma[modelKey].findMany.mockResolvedValue([]) +} + +describe('test moveBefore', () => { + it('should be moved', () => { + const arr = ['a', 'b', 'c'] + const arrSorted = moveBefore(arr, 'c', 'b') + expect(arrSorted).toEqual(['a', 'c', 'b']) + + const arrSorted2 = moveBefore(arr, 'c', 'a') + expect(arrSorted2).toEqual(['c', 'a', 'b']) + }) + it('should not be moved', () => { + const arr = ['a', 'b', 'c'] + const arrSorted = moveBefore(arr, 'b', 'c') + expect(arrSorted).toEqual(false) + + const arrSorted2 = moveBefore(arr, 'a', 'c') + expect(arrSorted2).toEqual(false) + + const arrSorted3 = moveBefore(arr, 'c', 'c') + expect(arrSorted3).toEqual(false) + }) +}) + +it('test resourceListToDict (by name)', () => { + const list = [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 }, + { name: 'c', value: 3 }, + ] + const dict = resourceListToDict(list) + expect(dict).toEqual({ + a: { name: 'a', value: 1 }, + b: { name: 'b', value: 2 }, + c: { name: 'c', value: 3 }, + }) +}) + +it('stringify bigint', () => { + const list = { name: 'a', value: 1n } + + const dict = JSON.stringify(list) + + expect(dict).toEqual('{"name":"a","value":"1n"}') +}) diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.ts b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.ts new file mode 100644 index 000000000..95924751a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.ts @@ -0,0 +1,85 @@ +// @ts-nocheck +import { Prisma } from '@prisma/client' + +// eslint-disable-next-line no-extend-native +BigInt.prototype.toJSON = function () { + return `${this.toString()}n` +} + +export type ResourceByName = Record +export function resourceListToDict(resList: Array): ResourceByName { + return resList.reduce((acc, curr) => { + return { + ...acc, + [curr.name]: curr, + } + }, {} as ResourceByName) +} + +// @ts-ignore +const Models = resourceListToDict(Prisma.dmmf.datamodel.models) +let ModelsNames = Object.keys(Models) +let ModelsOrder = [...ModelsNames] + +export function moveBefore(arr: T, toMove: T[number], ref: T[number]): T | false { + const iref = arr.indexOf(ref) + const moveref = arr.indexOf(toMove) + if (moveref <= iref) return false + return [ + ...arr.slice(0, iref), + arr[moveref], + ...arr.slice(iref, moveref), + ...arr.slice(moveref + 1), + ] as T +} + +export const manyToManyRelation: [string, string, string][] = [] +function sort() { + let hasChanged = false + for (const model of ModelsNames) { + for (const field of Models[model].fields) { + if (field.isId) Models[model].id = field.name + if (field.type in Models) { + const relationField = Models[field.type].fields.find(({ type }) => type === model) + if (!relationField) throw new Error('unable to find matching model') + if ( + (relationField.isRequired && field.isRequired && !relationField.isList) + || (relationField.isRequired && !field.isRequired) + ) { + const moveRes = moveBefore(ModelsOrder, model, field.type) + if (moveRes) { + hasChanged = true + ModelsOrder = moveRes + } + } + if ( + field.isList && relationField.isList + && !manyToManyRelation.find(test => + (test[0] === model && test[1] === field.type) || (test[0] === field.type && test[1] === model)) + ) { + manyToManyRelation.push([model, field.type, field.name]) + } + } + } + } + ModelsNames = ModelsOrder + if (hasChanged) sort() +} + +sort() + +// special case to study +const logUserCase = moveBefore(ModelsOrder, 'User', 'Log') +if (logUserCase) { + ModelsOrder = logUserCase +} +const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log') +if (logProjectCase) { + ModelsOrder = logProjectCase +} + +export const models: Record = {} +export const associations: Record = [] +export const modelKeys = ModelsOrder.map(model => model.slice(0, 1).toLocaleLowerCase() + model.slice(1)) From ab0793b9f2cdfd34d274b472cdb1e30cab9dce8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 15 Dec 2025 15:43:35 +0100 Subject: [PATCH 22/33] chore(server-nestjs): add code to PluginManagementService --- .../plugin-management.service.ts | 82 +++++++++++++++++- .../src/cpin-module/old-server/src/plugins.ts | 86 +++++++++---------- .../old-server/src/utils/plugins.ts | 16 ++-- 3 files changed, 132 insertions(+), 52 deletions(-) diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts index 02722c011..191130f6a 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts @@ -1,4 +1,84 @@ +import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; +import { plugin as argo } from '@cpn-console/argocd-plugin'; +import { plugin as gitlab } from '@cpn-console/gitlab-plugin'; +import { plugin as harbor } from '@cpn-console/harbor-plugin'; +import { + type Plugin, + PluginManagerOptions, + pluginManager, +} from '@cpn-console/hooks'; +import { plugin as keycloak } from '@cpn-console/keycloak-plugin'; +import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin'; +import { plugin as nexus } from '@cpn-console/nexus-plugin'; +import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin'; +import { plugin as vault } from '@cpn-console/vault-plugin'; import { Injectable } from '@nestjs/common'; +import { readdirSync, statSync } from 'node:fs'; @Injectable() -export class PluginManagementService {} +export class PluginManagementService { + constructor(private readonly configurationService: ConfigurationService) {} + + async initPm() { + const pluginManagerOptions: PluginManagerOptions = { + mockHooks: + this.configurationService.isCI || + (!this.configurationService.isProd && + !this.configurationService.isInt), + mockMonitoring: + this.configurationService.isCI || + (!this.configurationService.isProd && + !this.configurationService.isInt), + mockExternalServices: + this.configurationService.isCI || + (!this.configurationService.isProd && + !this.configurationService.isInt), + startPlugins: + (!this.configurationService.isCI && + this.configurationService.isProd) || + this.configurationService.isInt, + }; + const pm = pluginManager(pluginManagerOptions); + pm.register(argo); + pm.register(gitlab); + pm.register(harbor); + pm.register(keycloak); + pm.register(kubernetes); + pm.register(nexus); + pm.register(sonarqube); + pm.register(vault); + + if ( + !statSync(this.configurationService.pluginsDir, { + throwIfNoEntry: false, + }) + ) { + return pm; + } + for (const dirName of readdirSync( + this.configurationService.pluginsDir, + )) { + const moduleAbsPath = `${this.configurationService.pluginsDir}/${dirName}`; + try { + statSync(`${moduleAbsPath}/package.json`); + const pkg = await import(`${moduleAbsPath}/package.json`, { + with: { type: 'json' }, + }); + const entrypoint = pkg.default.module || pkg.default.main; + if (!entrypoint) + throw new Error( + `No entrypoint found in package.json : ${pkg.default.name}`, + ); + const { plugin } = (await import( + `${moduleAbsPath}/${entrypoint}` + )) as { plugin: Plugin }; + pm.register(plugin); + } catch (error) { + console.error(`Could not import module ${moduleAbsPath}`); + console.error(error.stack); + } + } + + return pm; + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts index eef052482..d18cfcf39 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts @@ -1,46 +1,46 @@ -import { readdirSync, statSync } from 'node:fs' -import { type Plugin, pluginManager } from '@cpn-console/hooks' -import { plugin as argo } from '@cpn-console/argocd-plugin' -import { plugin as gitlab } from '@cpn-console/gitlab-plugin' -import { plugin as harbor } from '@cpn-console/harbor-plugin' -import { plugin as keycloak } from '@cpn-console/keycloak-plugin' -import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin' -import { plugin as nexus } from '@cpn-console/nexus-plugin' -import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin' -import { plugin as vault } from '@cpn-console/vault-plugin' -import { pluginManagerOptions } from './utils/plugins' -import { pluginsDir } from './utils/env' +// import { readdirSync, statSync } from 'node:fs' +// import { type Plugin, pluginManager } from '@cpn-console/hooks' +// import { plugin as argo } from '@cpn-console/argocd-plugin' +// import { plugin as gitlab } from '@cpn-console/gitlab-plugin' +// import { plugin as harbor } from '@cpn-console/harbor-plugin' +// import { plugin as keycloak } from '@cpn-console/keycloak-plugin' +// import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin' +// import { plugin as nexus } from '@cpn-console/nexus-plugin' +// import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin' +// import { plugin as vault } from '@cpn-console/vault-plugin' +// import { pluginManagerOptions } from './utils/plugins' +// import { pluginsDir } from './utils/env' -export async function initPm() { - const pm = pluginManager(pluginManagerOptions) - pm.register(argo) - pm.register(gitlab) - pm.register(harbor) - pm.register(keycloak) - pm.register(kubernetes) - pm.register(nexus) - pm.register(sonarqube) - pm.register(vault) +// export async function initPm() { + // const pm = pluginManager(pluginManagerOptions) + // pm.register(argo) + // pm.register(gitlab) + // pm.register(harbor) + // pm.register(keycloak) + // pm.register(kubernetes) + // pm.register(nexus) + // pm.register(sonarqube) + // pm.register(vault) - if (!statSync(pluginsDir, { - throwIfNoEntry: false, - })) { - return pm - } - for (const dirName of readdirSync(pluginsDir)) { - const moduleAbsPath = `${pluginsDir}/${dirName}` - try { - statSync(`${moduleAbsPath}/package.json`) - const pkg = await import(`${moduleAbsPath}/package.json`, { with: { type: 'json' } }) - const entrypoint = pkg.default.module || pkg.default.main - if (!entrypoint) throw new Error(`No entrypoint found in package.json : ${pkg.default.name}`) - const { plugin } = await import(`${moduleAbsPath}/${entrypoint}`) as { plugin: Plugin } - pm.register(plugin) - } catch (error) { - console.error(`Could not import module ${moduleAbsPath}`) - console.error(error.stack) - } - } + // if (!statSync(pluginsDir, { + // throwIfNoEntry: false, + // })) { + // return pm + // } + // for (const dirName of readdirSync(pluginsDir)) { + // const moduleAbsPath = `${pluginsDir}/${dirName}` + // try { + // statSync(`${moduleAbsPath}/package.json`) + // const pkg = await import(`${moduleAbsPath}/package.json`, { with: { type: 'json' } }) + // const entrypoint = pkg.default.module || pkg.default.main + // if (!entrypoint) throw new Error(`No entrypoint found in package.json : ${pkg.default.name}`) + // const { plugin } = await import(`${moduleAbsPath}/${entrypoint}`) as { plugin: Plugin } + // pm.register(plugin) + // } catch (error) { + // console.error(`Could not import module ${moduleAbsPath}`) + // console.error(error.stack) + // } + // } - return pm -} + // return pm +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts index 6fe145941..f2f566967 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts @@ -1,9 +1,9 @@ -import type { PluginManagerOptions } from '@cpn-console/hooks' -import { isCI, isInt, isProd } from './env' +// import type { PluginManagerOptions } from '@cpn-console/hooks' +// import { isCI, isInt, isProd } from './env' -export const pluginManagerOptions: PluginManagerOptions = { - mockHooks: isCI || (!isProd && !isInt), - mockMonitoring: isCI || (!isProd && !isInt), - mockExternalServices: isCI || (!isProd && !isInt), - startPlugins: (!isCI && isProd) || isInt, -} +// export const pluginManagerOptions: PluginManagerOptions = { + // mockHooks: isCI || (!isProd && !isInt), + // mockMonitoring: isCI || (!isProd && !isInt), + // mockExternalServices: isCI || (!isProd && !isInt), + // startPlugins: (!isCI && isProd) || isInt, +// } From 76bdfb6c5e6fd9e56e1f5e7cc98d8d99b400879a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 15 Dec 2025 15:57:57 +0100 Subject: [PATCH 23/33] chore(server-nestjs): add code to DatabaseService and thus finalize ApplicationInitializationService --- apps/server-nestjs/README.md | 6 ++ .../application-initialization.service.ts | 69 +++----------- .../application-initialization.module.ts | 3 +- .../database/database.service.ts | 70 +++++++++++++- .../src/cpin-module/old-server/src/connect.ts | 92 +++++++++---------- apps/server-nestjs/src/prisma.ts | 5 + 6 files changed, 140 insertions(+), 105 deletions(-) create mode 100644 apps/server-nestjs/src/prisma.ts diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md index 94856fe50..6053eefc1 100644 --- a/apps/server-nestjs/README.md +++ b/apps/server-nestjs/README.md @@ -239,8 +239,14 @@ server-nestjs/$ find src/cpin-module/old-server -type f -iname "*.ts" -exec sed ## To delete (once we have a sastifying nestjs implementation): +Some `old-server` files are being rewritten and incorporated as NestJS modules. +We will keep track of them here so that we can go back and forth between the previous +implementation and the future NestJS one. In the meantime their code is commented out +in order to show if they can be safely removed (no import errors elsewhere) + ``` old-server/src/utils/logger.ts -> Replaced by LoggerModule old-server/src/utils/env.ts -> Replaced by ConfigurationModule old-server/src/init/db/* (except dump.ts) -> Replaced by DatabaseInitializationService +old-server/src/connect.ts -> Replaced by DatabaseService ``` diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts index f34ff379d..71cf184a1 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts @@ -1,17 +1,21 @@ +import { DatabaseService } from '@/cpin-module/infrastructure/database/database.service'; import { Injectable, Logger } from '@nestjs/common'; import { rm } from 'node:fs/promises'; import { resolve } from 'node:path'; import { ConfigurationService } from 'src/cpin-module/infrastructure/configuration/configuration.service'; -import app from './app'; -import { getConnection } from './connect'; -import { initDb } from './init/db/index'; -import { initPm } from './plugins'; +import { DatabaseInitializationService } from '../database-initialization/database-initialization.service'; +import { PluginManagementService } from '../plugin-management/plugin-management.service'; @Injectable() export class ApplicationInitializationService { private readonly logger = new Logger(ApplicationInitializationService.name); - constructor(private readonly config: ConfigurationService) {} + constructor( + private readonly config: ConfigurationService, + private readonly pluginManagementService: PluginManagementService, + private readonly databaseInitializationService: DatabaseInitializationService, + private readonly databaseService: DatabaseService, + ) {} async setUpHTTPProxy() { // Workaround because fetch isn't using http_proxy variables @@ -27,20 +31,19 @@ export class ApplicationInitializationService { async injectDataInDatabase(path: string) { this.logger.log('Starting init DB...'); const { data } = await import(path); - await initDb(data); + await this.databaseInitializationService.initDb(data); this.logger.log('initDb invoked successfully'); } - async startServer() { + async initApp() { try { - await getConnection(); + await this.databaseService.getConnection(); } catch (error) { - if (!(error instanceof Error)) return; this.logger.error(error.message); throw error; } - initPm(); + this.pluginManagementService.initPm(); this.logger.log('Reading init database file'); @@ -76,50 +79,4 @@ export class ApplicationInitializationService { isProd: this.config.isProd, }); } - - async getPreparedApp() { - try { - await getConnection(); - } catch (error) { - this.logger.error(error.message); - throw error; - } - - initPm(); - - this.logger.log('Reading init database file'); - - try { - const dataPath = - this.config.isProd || this.config.isInt - ? './init/db/imports/data' - : '@cpn-console/test-utils/src/imports/data'; - await this.injectDataInDatabase(dataPath); - if (this.config.isProd && !this.config.isDevSetup) { - this.logger.log('Cleaning up imported data file...'); - await rm(resolve(__dirname, dataPath)); - this.logger.log(`Successfully deleted '${dataPath}'`); - } - } catch (error) { - if ( - error.code === 'ERR_MODULE_NOT_FOUND' || - error.message.includes('Failed to load') || - error.message.includes('Cannot find module') - ) { - this.logger.log('No initDb file, skipping'); - } else { - this.logger.warn(error.message); - throw error; - } - } - - this.logger.debug({ - isDev: this.config.isDev, - isTest: this.config.isTest, - isCI: this.config.isCI, - isDevSetup: this.config.isDevSetup, - isProd: this.config.isProd, - }); - return app; - } } diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts index f361ffc82..f8405b2c2 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { ConfigurationModule } from '../infrastructure/configuration/configuration.module'; +import { InfrastructureModule } from '../infrastructure/infrastructure.module'; import { ApplicationInitializationService } from './application-initialization-service/application-initialization.service'; import { DatabaseInitializationService } from './database-initialization/database-initialization.service'; import { PluginManagementService } from './plugin-management/plugin-management.service'; @Module({ - imports: [ConfigurationModule], + imports: [ConfigurationModule, InfrastructureModule], providers: [ ApplicationInitializationService, DatabaseInitializationService, diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts index f0ff1df3a..1c61bd1e4 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts @@ -1,4 +1,70 @@ -import { Injectable } from '@nestjs/common'; +import prisma from '@/prisma'; +import { Injectable, Logger } from '@nestjs/common'; +import { setTimeout } from 'node:timers/promises'; + +import { ConfigurationService } from '../configuration/configuration.service'; @Injectable() -export class DatabaseService {} +export class DatabaseService { + constructor( + private readonly configurationService: ConfigurationService, + private readonly loggerService = new Logger(DatabaseService.name), + ) { + this.DELAY_BEFORE_RETRY = + this.configurationService.isTest || this.configurationService.isCI + ? 1000 + : 10000; + } + DELAY_BEFORE_RETRY!: number; + closingConnections = false; + + async getConnection(triesLeft = 5): Promise { + if (this.closingConnections || triesLeft <= 0) { + throw new Error('Unable to connect to Postgres server'); + } + triesLeft--; + + try { + if ( + this.configurationService.isDev || + this.configurationService.isTest || + this.configurationService.isCI + ) { + this.loggerService.log( + `Trying to connect to Postgres with: ${this.configurationService.dbUrl}`, + ); + } + await prisma.$connect(); + + this.loggerService.log('Connected to Postgres!'); + } catch (error) { + if (triesLeft > 0) { + this.loggerService.error(error); + this.loggerService.log( + `Could not connect to Postgres: ${error.message}`, + ); + this.loggerService.log(`Retrying (${triesLeft} tries left)`); + await setTimeout(this.DELAY_BEFORE_RETRY); + return this.getConnection(triesLeft); + } + + this.loggerService.log( + `Could not connect to Postgres: ${error.message}`, + ); + this.loggerService.log('Out of retries'); + error.message = `Out of retries, last error: ${error.message}`; + throw error; + } + } + + async closeConnections() { + this.closingConnections = true; + try { + await prisma.$disconnect(); + } catch (error) { + this.loggerService.error(error); + } finally { + this.closingConnections = false; + } + } +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts index ebd23ad3c..dd673184d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts @@ -1,52 +1,52 @@ -import { setTimeout } from 'node:timers/promises' -import prisma from './prisma' -import { logger } from './app' -import { - dbUrl, - isCI, - isDev, - isTest, -} from './utils/env' +// import { setTimeout } from 'node:timers/promises' +// import prisma from './prisma' +// import { logger } from './app' +// import { + // dbUrl, + // isCI, + // isDev, + // isTest, +// } from './utils/env' -const DELAY_BEFORE_RETRY = isTest || isCI ? 1000 : 10000 -let closingConnections = false +// const DELAY_BEFORE_RETRY = isTest || isCI ? 1000 : 10000 +// let closingConnections = false -export async function getConnection(triesLeft = 5): Promise { - if (closingConnections || triesLeft <= 0) { - throw new Error('Unable to connect to Postgres server') - } - triesLeft-- +// export async function getConnection(triesLeft = 5): Promise { + // if (closingConnections || triesLeft <= 0) { + // throw new Error('Unable to connect to Postgres server') + // } + // triesLeft-- - try { - if (isDev || isTest || isCI) { - logger.info(`Trying to connect to Postgres with: ${dbUrl}`) - } - await prisma.$connect() + // try { + // if (isDev || isTest || isCI) { + // logger.info(`Trying to connect to Postgres with: ${dbUrl}`) + // } + // await prisma.$connect() - logger.info('Connected to Postgres!') - } catch (error) { - if (triesLeft > 0) { - logger.error(error) - logger.info(`Could not connect to Postgres: ${error.message}`) - logger.info(`Retrying (${triesLeft} tries left)`) - await setTimeout(DELAY_BEFORE_RETRY) - return getConnection(triesLeft) - } + // logger.info('Connected to Postgres!') + // } catch (error) { + // if (triesLeft > 0) { + // logger.error(error) + // logger.info(`Could not connect to Postgres: ${error.message}`) + // logger.info(`Retrying (${triesLeft} tries left)`) + // await setTimeout(DELAY_BEFORE_RETRY) + // return getConnection(triesLeft) + // } - logger.info(`Could not connect to Postgres: ${error.message}`) - logger.info('Out of retries') - error.message = `Out of retries, last error: ${error.message}` - throw error - } -} + // logger.info(`Could not connect to Postgres: ${error.message}`) + // logger.info('Out of retries') + // error.message = `Out of retries, last error: ${error.message}` + // throw error + // } +// } -export async function closeConnections() { - closingConnections = true - try { - await prisma.$disconnect() - } catch (error) { - logger.error(error) - } finally { - closingConnections = false - } -} +// export async function closeConnections() { + // closingConnections = true + // try { + // await prisma.$disconnect() + // } catch (error) { + // logger.error(error) + // } finally { + // closingConnections = false + // } +// } diff --git a/apps/server-nestjs/src/prisma.ts b/apps/server-nestjs/src/prisma.ts new file mode 100644 index 000000000..4590932b6 --- /dev/null +++ b/apps/server-nestjs/src/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +export default prisma From e0d3d3048c61857bcaaaef41ff00d056f80085d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 15 Dec 2025 16:05:29 +0100 Subject: [PATCH 24/33] chore(server-nestjs): rename vite file to avoid compilation issues for now --- apps/server-nestjs/README.md | 1 + .../application-initialization.service.ts | 36 +++- .../cpin-module/old-server/src/prepare-app.ts | 188 +++++++++--------- .../src/cpin-module/old-server/src/server.ts | 88 ++++---- .../{vite.config.ts => vite.config.ts.backup} | 0 .../{vitest-init.ts => vitest-init.ts.backup} | 0 ...test.config.ts => vitest.config.ts.backup} | 0 7 files changed, 174 insertions(+), 139 deletions(-) rename apps/server-nestjs/src/cpin-module/old-server/{vite.config.ts => vite.config.ts.backup} (100%) rename apps/server-nestjs/src/cpin-module/old-server/{vitest-init.ts => vitest-init.ts.backup} (100%) rename apps/server-nestjs/src/cpin-module/old-server/{vitest.config.ts => vitest.config.ts.backup} (100%) diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md index 6053eefc1..9f55b1ee3 100644 --- a/apps/server-nestjs/README.md +++ b/apps/server-nestjs/README.md @@ -249,4 +249,5 @@ old-server/src/utils/logger.ts -> Replaced by LoggerModule old-server/src/utils/env.ts -> Replaced by ConfigurationModule old-server/src/init/db/* (except dump.ts) -> Replaced by DatabaseInitializationService old-server/src/connect.ts -> Replaced by DatabaseService +old-server/src/server.ts -> Incorporated in ApplicationInitializationService ``` diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts index 71cf184a1..ff2b81de1 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts @@ -15,7 +15,9 @@ export class ApplicationInitializationService { private readonly pluginManagementService: PluginManagementService, private readonly databaseInitializationService: DatabaseInitializationService, private readonly databaseService: DatabaseService, - ) {} + ) { + this.handleExit(); + } async setUpHTTPProxy() { // Workaround because fetch isn't using http_proxy variables @@ -79,4 +81,36 @@ export class ApplicationInitializationService { isProd: this.config.isProd, }); } + + async exitGracefully(error?: Error) { + if (error instanceof Error) { + this.logger.fatal(error); + } + // @TODO Determine if it is necessary, or if we would rather plug ourselves + // onto NestJS lifecycle, or even if all this is actually necessary + // at all anymore + // + // await app.close(); + + this.logger.log('Closing connections...'); + await this.databaseService.closeConnections(); + this.logger.log('Exiting...'); + process.exit(error instanceof Error ? 1 : 0); + } + + logExitCode(code: number) { + this.logger.warn(`received signal: ${code}`); + } + + logUnhandledRejection(reason: unknown, promise: Promise) { + this.logger.error({ message: 'Unhandled Rejection', promise, reason }); + } + + handleExit() { + process.on('exit', this.logExitCode); + process.on('SIGINT', this.exitGracefully); + process.on('SIGTERM', this.exitGracefully); + process.on('uncaughtException', this.exitGracefully); + process.on('unhandledRejection', this.logUnhandledRejection); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts index 5be3a06ba..b2ab294f9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts @@ -1,106 +1,106 @@ -import { rm } from 'node:fs/promises' -import { dirname, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' -import { isCI, isDev, isDevSetup, isInt, isProd, isTest, port } from './utils/env' -import app, { logger } from './app' -import { getConnection } from './connect' -import { initDb } from './init/db/index' -import { initPm } from './plugins' +// import { rm } from 'node:fs/promises' +// import { dirname, resolve } from 'node:path' +// import { fileURLToPath } from 'node:url' +// import { isCI, isDev, isDevSetup, isInt, isProd, isTest, port } from './utils/env' +// import app, { logger } from './app' +// import { getConnection } from './connect' +// import { initDb } from './init/db/index' +// import { initPm } from './plugins' -// Workaround because fetch isn't using http_proxy variables -// See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 -if (process.env.HTTP_PROXY) { - const Undici = await import('undici') - const ProxyAgent = Undici.ProxyAgent - const setGlobalDispatcher = Undici.setGlobalDispatcher - setGlobalDispatcher( - new ProxyAgent(process.env.HTTP_PROXY), - ) -} +// // Workaround because fetch isn't using http_proxy variables +// // See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 +// if (process.env.HTTP_PROXY) { + // const Undici = await import('undici') + // const ProxyAgent = Undici.ProxyAgent + // const setGlobalDispatcher = Undici.setGlobalDispatcher + // setGlobalDispatcher( + // new ProxyAgent(process.env.HTTP_PROXY), + // ) +// } -async function initializeDB(path: string) { - logger.info('Starting init DB...') - const { data } = await import(path) - await initDb(data) - logger.info('initDb invoked successfully') -} +// async function initializeDB(path: string) { + // logger.info('Starting init DB...') + // const { data } = await import(path) + // await initDb(data) + // logger.info('initDb invoked successfully') +// } -export async function startServer(defaultPort: number = (port ? +port : 8080)) { - try { - await getConnection() - } catch (error) { - if (!(error instanceof Error)) return - logger.error(error.message) - throw error - } +// export async function startServer(defaultPort: number = (port ? +port : 8080)) { + // try { + // await getConnection() + // } catch (error) { + // if (!(error instanceof Error)) return + // logger.error(error.message) + // throw error + // } - initPm() + // initPm() - logger.info('Reading init database file') + // logger.info('Reading init database file') - try { - const dataPath = (isProd || isInt) - ? './init/db/imports/data' - : '@cpn-console/test-utils/src/imports/data' - await initializeDB(dataPath) - if (isProd && !isDevSetup) { - logger.info('Cleaning up imported data file...') - const __filename = fileURLToPath(import.meta.url) - const __dirname = dirname(__filename) - await rm(resolve(__dirname, dataPath)) - logger.info(`Successfully deleted '${dataPath}'`) - } - } catch (error) { - if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { - logger.info('No initDb file, skipping') - } else { - logger.warn(error.message) - throw error - } - } + // try { + // const dataPath = (isProd || isInt) + // ? './init/db/imports/data' + // : '@cpn-console/test-utils/src/imports/data' + // await initializeDB(dataPath) + // if (isProd && !isDevSetup) { + // logger.info('Cleaning up imported data file...') + // const __filename = fileURLToPath(import.meta.url) + // const __dirname = dirname(__filename) + // await rm(resolve(__dirname, dataPath)) + // logger.info(`Successfully deleted '${dataPath}'`) + // } + // } catch (error) { + // if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { + // logger.info('No initDb file, skipping') + // } else { + // logger.warn(error.message) + // throw error + // } + // } - try { - await app.listen({ host: '0.0.0.0', port: defaultPort ?? 8080 }) - } catch (error) { - logger.error(error) - process.exit(1) - } - logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) -} + // try { + // await app.listen({ host: '0.0.0.0', port: defaultPort ?? 8080 }) + // } catch (error) { + // logger.error(error) + // process.exit(1) + // } + // logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) +// } -export async function getPreparedApp() { - try { - await getConnection() - } catch (error) { - logger.error(error.message) - throw error - } +// export async function getPreparedApp() { + // try { + // await getConnection() + // } catch (error) { + // logger.error(error.message) + // throw error + // } - initPm() + // initPm() - logger.info('Reading init database file') + // logger.info('Reading init database file') - try { - const dataPath = (isProd || isInt) - ? './init/db/imports/data' - : '@cpn-console/test-utils/src/imports/data' - await initializeDB(dataPath) - if (isProd && !isDevSetup) { - logger.info('Cleaning up imported data file...') - const __filename = fileURLToPath(import.meta.url) - const __dirname = dirname(__filename) - await rm(resolve(__dirname, dataPath)) - logger.info(`Successfully deleted '${dataPath}'`) - } - } catch (error) { - if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { - logger.info('No initDb file, skipping') - } else { - logger.warn(error.message) - throw error - } - } + // try { + // const dataPath = (isProd || isInt) + // ? './init/db/imports/data' + // : '@cpn-console/test-utils/src/imports/data' + // await initializeDB(dataPath) + // if (isProd && !isDevSetup) { + // logger.info('Cleaning up imported data file...') + // const __filename = fileURLToPath(import.meta.url) + // const __dirname = dirname(__filename) + // await rm(resolve(__dirname, dataPath)) + // logger.info(`Successfully deleted '${dataPath}'`) + // } + // } catch (error) { + // if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { + // logger.info('No initDb file, skipping') + // } else { + // logger.warn(error.message) + // throw error + // } + // } - logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) - return app -} + // logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) + // return app +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts index 577540cac..d42849fb5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts @@ -1,44 +1,44 @@ -import { getPreparedApp } from './prepare-app' -import { closeConnections } from './connect' -import { isCI, isDev, isDevSetup, isProd, isTest, port } from './utils/env' -import { logger } from './app' - -const app = await getPreparedApp() - -try { - await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }) -} catch (error) { - logger.error(error) - process.exit(1) -} - -logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) - -export async function exitGracefully(error?: Error) { - if (error instanceof Error) { - logger.fatal(error) - } - await app.close() - logger.info('Closing connections...') - await closeConnections() - logger.info('Exiting...') - process.exit(error instanceof Error ? 1 : 0) -} - -function logExitCode(code: number) { - logger.warn(`received signal: ${code}`) -} - -function logUnhandledRejection(reason: unknown, promise: Promise) { - logger.error({ message: 'Unhandled Rejection', promise, reason }) -} - -export function handleExit() { - process.on('exit', logExitCode) - process.on('SIGINT', exitGracefully) - process.on('SIGTERM', exitGracefully) - process.on('uncaughtException', exitGracefully) - process.on('unhandledRejection', logUnhandledRejection) -} - -handleExit() +// import { getPreparedApp } from './prepare-app' +// import { closeConnections } from './connect' +// import { isCI, isDev, isDevSetup, isProd, isTest, port } from './utils/env' +// import { logger } from './app' + +// const app = await getPreparedApp() + +// try { + // await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }) +// } catch (error) { + // logger.error(error) + // process.exit(1) +// } + +// logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) + +// export async function exitGracefully(error?: Error) { + // if (error instanceof Error) { + // logger.fatal(error) + // } + // await app.close() + // logger.info('Closing connections...') + // await closeConnections() + // logger.info('Exiting...') + // process.exit(error instanceof Error ? 1 : 0) +// } + +// function logExitCode(code: number) { + // logger.warn(`received signal: ${code}`) +// } + +// function logUnhandledRejection(reason: unknown, promise: Promise) { + // logger.error({ message: 'Unhandled Rejection', promise, reason }) +// } + +// export function handleExit() { + // process.on('exit', logExitCode) + // process.on('SIGINT', exitGracefully) + // process.on('SIGTERM', exitGracefully) + // process.on('uncaughtException', exitGracefully) + // process.on('unhandledRejection', logUnhandledRejection) +// } + +// handleExit() diff --git a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts.backup similarity index 100% rename from apps/server-nestjs/src/cpin-module/old-server/vite.config.ts rename to apps/server-nestjs/src/cpin-module/old-server/vite.config.ts.backup diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts.backup similarity index 100% rename from apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts rename to apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts.backup diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts.backup similarity index 100% rename from apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts rename to apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts.backup From aafff4c1ba64d3fcdb022e9b4cb3578c0a07f340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 15 Dec 2025 16:49:30 +0100 Subject: [PATCH 25/33] chore(server-nestjs): initialize core module and its AppService --- .../cpin-module/core/app/app.service.spec.ts | 18 ++ .../src/cpin-module/core/app/app.service.ts | 158 ++++++++++++++++++ .../src/cpin-module/core/core.module.ts | 10 ++ .../src/cpin-module/cpin.module.ts | 7 +- .../src/cpin-module/old-server/src/app.ts | 108 ++++++------ .../old-server/src/utils/keycloak-utils.ts | 48 +++--- .../old-server/src/utils/keycloak.ts | 80 ++++----- 7 files changed, 310 insertions(+), 119 deletions(-) create mode 100644 apps/server-nestjs/src/cpin-module/core/app/app.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/app/app.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/core.module.ts diff --git a/apps/server-nestjs/src/cpin-module/core/app/app.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/app/app.service.spec.ts new file mode 100644 index 000000000..0d0025276 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/app/app.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppService } from './app.service'; + +describe('AppService', () => { + let service: AppService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AppService], + }).compile(); + + service = module.get(AppService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts new file mode 100644 index 000000000..6d32ab8f4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts @@ -0,0 +1,158 @@ +import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; +import { apiPrefix, getContract } from '@cpn-console/shared'; +import { + serviceContract, + swaggerUiPath, + systemContract, +} from '@cpn-console/shared'; +import { tokenHeaderName } from '@cpn-console/shared'; +import fastifyCookie from '@fastify/cookie'; +import helmet from '@fastify/helmet'; +import fastifySession, { FastifySessionOptions } from '@fastify/session'; +import fastifySwagger from '@fastify/swagger'; +import fastifySwaggerUi from '@fastify/swagger-ui'; +import { Injectable, Logger } from '@nestjs/common'; +import { initServer } from '@ts-rest/fastify'; +import { generateOpenApi } from '@ts-rest/open-api'; +import fastify from 'fastify'; +import type { FastifyRequest } from 'fastify'; +import keycloak, { KeycloakOptions } from 'fastify-keycloak-adapter'; + +import { apiRouter } from './resources/index'; +import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify'; + +interface KeycloakPayload { + sub: string; + email: string; + given_name: string; + family_name: string; + groups: string[]; +} + +function userPayloadMapper(userPayload: KeycloakPayload) { + return { + id: userPayload.sub, + email: userPayload.email, + firstName: userPayload.given_name, + lastName: userPayload.family_name, + groups: userPayload.groups || [], + }; +} + +function bypassFn(request: FastifyRequest) { + try { + return !!request.headers[tokenHeaderName]; + } catch (_e) {} + return false; +} + +@Injectable() +export class AppService { + constructor( + private readonly configurationService: ConfigurationService, + + private readonly loggerService = new Logger(AppService.name), + ) { + this.keycloakConf = { + appOrigin: + this.configurationService.keycloakRedirectUri ?? + 'http://localhost:8080', + keycloakSubdomain: `${this.configurationService.keycloakDomain}/realms/${this.configurationService.keycloakRealm}`, + clientId: this.configurationService.keycloakClientId ?? '', + clientSecret: this.configurationService.keycloakClientSecret ?? '', + useHttps: this.configurationService.keycloakProtocol === 'https', + disableCookiePlugin: true, + disableSessionPlugin: true, + // @ts-ignore + userPayloadMapper, + retries: 5, + excludedPatterns: [ + systemContract.getVersion.path, + systemContract.getHealth.path, + serviceContract.getServiceHealth.path, + `${swaggerUiPath}/**`, + ], + bypassFn, + }; + + this.sessionConf = { + cookieName: 'sessionId', + secret: + this.configurationService.sessionSecret || + 'a-very-strong-secret-with-more-than-32-char', + cookie: { + httpOnly: true, + secure: true, + maxAge: 1_800_000, + }, + }; + } + + keycloakConf!: KeycloakOptions; + sessionConf!: FastifySessionOptions; + + async startApp() { + //@TODO is this still necessary ? + initServer(); + + const openApiDocument = generateOpenApi( + await getContract(), + swaggerConf, + { setOperationId: true }, + ); + + const app = fastify(fastifyConf) + .register(helmet, () => ({ + contentSecurityPolicy: !( + this.configurationService.isInt || + this.configurationService.isDev || + this.configurationService.isTest + ), + })) + .register(fastifyCookie) + .register(fastifySession, this.sessionConf) + // @ts-ignore + .register(keycloak, this.keycloakConf) + .register(fastifySwagger, { + transformObject: () => openApiDocument, + }) + .register(fastifySwaggerUi, swaggerUiConf) + .register(apiRouter()) + .addHook('onRoute', (opts) => { + if (opts.path === `${apiPrefix}/healthz`) { + opts.logLevel = 'silent'; + } + }) + .setErrorHandler((error: Error, req: { id: string }, reply) => { + const statusCode = 500; + // @ts-ignore vérifier l'objet + const message = error.description || error.message; + reply.status(statusCode).send({ + status: statusCode, + error: message, + stack: error.stack, + }); + this.loggerService.log('info', { reqId: req.id, error }); + }) + .addHook('onResponse', (req, res) => { + if (res.statusCode < 400) { + req.log.info({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } else if (res.statusCode < 500) { + req.log.warn({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } else { + req.log.error({ + status: res.statusCode, + userId: req.session?.user?.id, + }); + } + }); + + await app.ready(); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/core.module.ts b/apps/server-nestjs/src/cpin-module/core/core.module.ts new file mode 100644 index 000000000..195a053b9 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/core.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { ConfigurationModule } from '../infrastructure/configuration/configuration.module'; +import { AppService } from './app/app.service'; + +@Module({ + imports: [ConfigurationModule], + providers: [AppService], +}) +export class CoreModule {} diff --git a/apps/server-nestjs/src/cpin-module/cpin.module.ts b/apps/server-nestjs/src/cpin-module/cpin.module.ts index cced22024..804f4b84f 100644 --- a/apps/server-nestjs/src/cpin-module/cpin.module.ts +++ b/apps/server-nestjs/src/cpin-module/cpin.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { ApplicationInitializationModule } from './application-initialization/application-initialization.module'; +import { CoreModule } from './core/core.module'; import { InfrastructureModule } from './infrastructure/infrastructure.module'; // This module host the old "server code" of our backend. @@ -9,6 +10,10 @@ import { InfrastructureModule } from './infrastructure/infrastructure.module'; @Module({ controllers: [], providers: [], - imports: [ApplicationInitializationModule, InfrastructureModule], + imports: [ + ApplicationInitializationModule, + CoreModule, + InfrastructureModule, + ], }) export class CpinModule {} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts index 7b06c1608..1c4c2d09d 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts @@ -1,59 +1,59 @@ -import type { FastifyRequest } from 'fastify' -import fastify from 'fastify' -import helmet from '@fastify/helmet' -import keycloak from 'fastify-keycloak-adapter' -import fastifySession from '@fastify/session' -import fastifyCookie from '@fastify/cookie' -import fastifySwagger from '@fastify/swagger' -import fastifySwaggerUi from '@fastify/swagger-ui' -import { initServer } from '@ts-rest/fastify' -import { generateOpenApi } from '@ts-rest/open-api' -import { apiPrefix, getContract } from '@cpn-console/shared' -import { isDev, isInt, isTest } from './utils/env' -import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify' -import { apiRouter } from './resources/index' -import { keycloakConf, sessionConf } from './utils/keycloak' -import type { CustomLogger } from './utils/logger' -import { log } from './utils/logger' +// import type { FastifyRequest } from 'fastify' +// import fastify from 'fastify' +// import helmet from '@fastify/helmet' +// import keycloak from 'fastify-keycloak-adapter' +// import fastifySession from '@fastify/session' +// import fastifyCookie from '@fastify/cookie' +// import fastifySwagger from '@fastify/swagger' +// import fastifySwaggerUi from '@fastify/swagger-ui' +// import { initServer } from '@ts-rest/fastify' +// import { generateOpenApi } from '@ts-rest/open-api' +// import { apiPrefix, getContract } from '@cpn-console/shared' +// import { isDev, isInt, isTest } from './utils/env' +// import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify' +// import { apiRouter } from './resources/index' +// import { keycloakConf, sessionConf } from './utils/keycloak' +// import type { CustomLogger } from './utils/logger' +// import { log } from './utils/logger' -export const serverInstance: ReturnType = initServer() +// export const serverInstance: ReturnType = initServer() -const openApiDocument = generateOpenApi(await getContract(), swaggerConf, { setOperationId: true }) +// const openApiDocument = generateOpenApi(await getContract(), swaggerConf, { setOperationId: true }) -const app = fastify(fastifyConf) - .register(helmet, () => ({ - contentSecurityPolicy: !(isInt || isDev || isTest), - })) - .register(fastifyCookie) - .register(fastifySession, sessionConf) - // @ts-ignore - .register(keycloak, keycloakConf) - .register(fastifySwagger, { transformObject: () => openApiDocument }) - .register(fastifySwaggerUi, swaggerUiConf) - .register(apiRouter()) - .addHook('onRoute', (opts) => { - if (opts.path === `${apiPrefix}/healthz`) { - opts.logLevel = 'silent' - } - }) - .setErrorHandler((error: Error, req: FastifyRequest, reply) => { - const statusCode = 500 - // @ts-ignore vérifier l'objet - const message = error.description || error.message - reply.status(statusCode).send({ status: statusCode, error: message, stack: error.stack }) - log('info', { reqId: req.id, error }) - }) - .addHook('onResponse', (req, res) => { - if (res.statusCode < 400) { - req.log.info({ status: res.statusCode, userId: req.session?.user?.id }) - } else if (res.statusCode < 500) { - req.log.warn({ status: res.statusCode, userId: req.session?.user?.id }) - } else { - req.log.error({ status: res.statusCode, userId: req.session?.user?.id }) - } - }) +// const app = fastify(fastifyConf) + // .register(helmet, () => ({ + // contentSecurityPolicy: !(isInt || isDev || isTest), + // })) + // .register(fastifyCookie) + // .register(fastifySession, sessionConf) + // // @ts-ignore + // .register(keycloak, keycloakConf) + // .register(fastifySwagger, { transformObject: () => openApiDocument }) + // .register(fastifySwaggerUi, swaggerUiConf) + // .register(apiRouter()) + // .addHook('onRoute', (opts) => { + // if (opts.path === `${apiPrefix}/healthz`) { + // opts.logLevel = 'silent' + // } + // }) + // .setErrorHandler((error: Error, req: FastifyRequest, reply) => { + // const statusCode = 500 + // // @ts-ignore vérifier l'objet + // const message = error.description || error.message + // reply.status(statusCode).send({ status: statusCode, error: message, stack: error.stack }) + // log('info', { reqId: req.id, error }) + // }) + // .addHook('onResponse', (req, res) => { + // if (res.statusCode < 400) { + // req.log.info({ status: res.statusCode, userId: req.session?.user?.id }) + // } else if (res.statusCode < 500) { + // req.log.warn({ status: res.statusCode, userId: req.session?.user?.id }) + // } else { + // req.log.error({ status: res.statusCode, userId: req.session?.user?.id }) + // } + // }) -await app.ready() +// await app.ready() -export const logger = app.log as CustomLogger -export default app +// export const logger = app.log as CustomLogger +// export default app diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts index 462116029..029e9a2f5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts @@ -1,27 +1,27 @@ -import { tokenHeaderName } from '@cpn-console/shared' -import type { FastifyRequest } from 'fastify' +// import { tokenHeaderName } from '@cpn-console/shared' +// import type { FastifyRequest } from 'fastify' -interface KeycloakPayload { - sub: string - email: string - given_name: string - family_name: string - groups: string[] -} +// interface KeycloakPayload { + // sub: string + // email: string + // given_name: string + // family_name: string + // groups: string[] +// } -export function userPayloadMapper(userPayload: KeycloakPayload) { - return { - id: userPayload.sub, - email: userPayload.email, - firstName: userPayload.given_name, - lastName: userPayload.family_name, - groups: userPayload.groups || [], - } -} +// export function userPayloadMapper(userPayload: KeycloakPayload) { + // return { + // id: userPayload.sub, + // email: userPayload.email, + // firstName: userPayload.given_name, + // lastName: userPayload.family_name, + // groups: userPayload.groups || [], + // } +// } -export function bypassFn(request: FastifyRequest) { - try { - return !!request.headers[tokenHeaderName] - } catch (_e) {} - return false -} +// export function bypassFn(request: FastifyRequest) { + // try { + // return !!request.headers[tokenHeaderName] + // } catch (_e) {} + // return false +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts index 70905295e..a91fdde5f 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts @@ -1,42 +1,42 @@ -import { serviceContract, swaggerUiPath, systemContract } from '@cpn-console/shared' -import type { KeycloakOptions } from 'fastify-keycloak-adapter' -import { - keycloakClientId, - keycloakClientSecret, - keycloakDomain, - keycloakProtocol, - keycloakRealm, - keycloakRedirectUri, - sessionSecret, -} from './env' -import { bypassFn, userPayloadMapper } from './keycloak-utils' +// import { serviceContract, swaggerUiPath, systemContract } from '@cpn-console/shared' +// import type { KeycloakOptions } from 'fastify-keycloak-adapter' +// import { + // keycloakClientId, + // keycloakClientSecret, + // keycloakDomain, + // keycloakProtocol, + // keycloakRealm, + // keycloakRedirectUri, + // sessionSecret, +// } from './env' +// import { bypassFn, userPayloadMapper } from './keycloak-utils' -export const keycloakConf = { - appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', - keycloakSubdomain: `${keycloakDomain}/realms/${keycloakRealm}`, - clientId: keycloakClientId ?? '', - clientSecret: keycloakClientSecret ?? '', - useHttps: keycloakProtocol === 'https', - disableCookiePlugin: true, - disableSessionPlugin: true, - // @ts-ignore - userPayloadMapper, - retries: 5, - excludedPatterns: [ - systemContract.getVersion.path, - systemContract.getHealth.path, - serviceContract.getServiceHealth.path, - `${swaggerUiPath}/**`, - ], - bypassFn, -} as const satisfies KeycloakOptions +// export const keycloakConf = { + // appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', + // keycloakSubdomain: `${keycloakDomain}/realms/${keycloakRealm}`, + // clientId: keycloakClientId ?? '', + // clientSecret: keycloakClientSecret ?? '', + // useHttps: keycloakProtocol === 'https', + // disableCookiePlugin: true, + // disableSessionPlugin: true, + // // @ts-ignore + // userPayloadMapper, + // retries: 5, + // excludedPatterns: [ + // systemContract.getVersion.path, + // systemContract.getHealth.path, + // serviceContract.getServiceHealth.path, + // `${swaggerUiPath}/**`, + // ], + // bypassFn, +// } as const satisfies KeycloakOptions -export const sessionConf = { - cookieName: 'sessionId', - secret: sessionSecret || 'a-very-strong-secret-with-more-than-32-char', - cookie: { - httpOnly: true, - secure: true, - }, - expires: 1_800_000, -} +// export const sessionConf = { + // cookieName: 'sessionId', + // secret: sessionSecret || 'a-very-strong-secret-with-more-than-32-char', + // cookie: { + // httpOnly: true, + // secure: true, + // }, + // expires: 1_800_000, +// } From ee41addedaf8a2af24eb41c3659fdd3d6e924dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Thu, 18 Dec 2025 15:59:21 +0100 Subject: [PATCH 26/33] chore(server-nestjs): convert all router services to NestJS classes --- .../src/cpin-module/core/app/app.service.ts | 22 +- .../src/cpin-module/core/core.module.ts | 14 +- .../core/fastify/fastify.service.spec.ts | 18 + .../core/fastify/fastify.service.ts | 62 ++ .../admin-role-router.service.spec.ts | 18 + .../admin-role-router.service.ts | 83 +++ .../admin-token-router.service.spec.ts | 18 + .../admin-token-router.service.ts | 57 ++ .../cluster-router.service.spec.ts | 18 + .../cluster-router/cluster-router.service.ts | 160 +++++ .../environment-router.service.spec.ts | 18 + .../environment-router.service.ts | 155 +++++ .../log-router/log-router.service.spec.ts | 18 + .../router/log-router/log-router.service.ts | 45 ++ .../project-member-router.service.spec.ts | 18 + .../project-member-router.service.ts | 130 ++++ .../project-role-router.service.spec.ts | 18 + .../project-role-router.service.ts | 136 +++++ .../project-router.service.spec.ts | 18 + .../project-router/project-router.service.ts | 251 ++++++++ .../project-service-router.service.spec.ts | 18 + .../project-service-router.service.ts | 85 +++ .../repository-router.service.spec.ts | 18 + .../repository-router.service.ts | 199 ++++++ .../cpin-module/core/router/router.module.ts | 52 ++ .../core/router/router.service.spec.ts | 18 + .../cpin-module/core/router/router.service.ts | 171 ++++++ .../service-chain-router.service.spec.ts | 18 + .../service-chain-router.service.ts | 96 +++ .../service-monitor-router.service.spec.ts | 18 + .../service-monitor-router.service.ts | 54 ++ .../stage-router/stage-router.service.spec.ts | 18 + .../stage-router/stage-router.service.ts | 95 +++ .../system-config-router.service.spec.ts | 18 + .../system-config-router.service.ts | 46 ++ .../system-router.service.spec.ts | 18 + .../system-router/system-router.service.ts | 30 + .../system-settings-router.service.spec.ts | 18 + .../system-settings-router.service.ts | 43 ++ .../user-router/user-router.service.spec.ts | 18 + .../router/user-router/user-router.service.ts | 78 +++ .../user-tokens-router.service.spec.ts | 18 + .../user-tokens-router.service.ts | 67 +++ .../zone-router/zone-router.service.spec.ts | 18 + .../router/zone-router/zone-router.service.ts | 90 +++ .../src/cpin-module/cpin.module.ts | 2 - .../configuration/configuration.service.ts | 4 - .../database/database.service.ts | 7 +- .../infrastructure/infrastructure.module.ts | 14 +- .../server/server.service.spec.ts | 18 + .../infrastructure/server/server.service.ts | 13 + .../src/resources/admin-role/router.ts | 120 ++-- .../src/resources/admin-token/router.ts | 74 +-- .../src/resources/cluster/router.ts | 250 ++++---- .../src/resources/environment/router.ts | 192 +++--- .../old-server/src/resources/index.ts | 94 +-- .../old-server/src/resources/log/router.ts | 56 +- .../src/resources/project-member/router.ts | 164 ++--- .../src/resources/project-role/router.ts | 180 +++--- .../src/resources/project-service/router.ts | 64 +- .../src/resources/project/business.ts | 568 +++++++++++------- .../src/resources/project/queries.ts | 550 +++++++++-------- .../src/resources/project/router.ts | 398 ++++++------ .../src/resources/repository/router.ts | 276 ++++----- .../src/resources/service-chain/router.ts | 180 +++--- .../src/resources/service-monitor/router.ts | 86 +-- .../old-server/src/resources/stage/router.ts | 176 +++--- .../src/resources/system/config/router.ts | 60 +- .../old-server/src/resources/system/router.ts | 38 +- .../src/resources/system/settings/router.ts | 50 +- .../old-server/src/resources/user/router.ts | 126 ++-- .../src/resources/user/tokens/router.ts | 96 +-- .../old-server/src/resources/zone/router.ts | 102 ++-- .../old-server/src/utils/fastify.ts | 100 +-- .../cpin-module/old-server/src/utils/mocks.ts | 2 +- apps/server-nestjs/src/main.ts | 2 +- 76 files changed, 4726 insertions(+), 1935 deletions(-) create mode 100644 apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/router.module.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.spec.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.ts diff --git a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts index 6d32ab8f4..4533c1556 100644 --- a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts @@ -12,14 +12,13 @@ import fastifySession, { FastifySessionOptions } from '@fastify/session'; import fastifySwagger from '@fastify/swagger'; import fastifySwaggerUi from '@fastify/swagger-ui'; import { Injectable, Logger } from '@nestjs/common'; -import { initServer } from '@ts-rest/fastify'; import { generateOpenApi } from '@ts-rest/open-api'; import fastify from 'fastify'; import type { FastifyRequest } from 'fastify'; import keycloak, { KeycloakOptions } from 'fastify-keycloak-adapter'; -import { apiRouter } from './resources/index'; -import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify'; +import { FastifyService } from '../fastify/fastify.service'; +import { RouterService } from '../router/router.service'; interface KeycloakPayload { sub: string; @@ -48,10 +47,12 @@ function bypassFn(request: FastifyRequest) { @Injectable() export class AppService { + private readonly loggerService = new Logger(AppService.name); + constructor( private readonly configurationService: ConfigurationService, - - private readonly loggerService = new Logger(AppService.name), + private readonly routerService: RouterService, + private readonly fastifyService: FastifyService, ) { this.keycloakConf = { appOrigin: @@ -92,16 +93,13 @@ export class AppService { sessionConf!: FastifySessionOptions; async startApp() { - //@TODO is this still necessary ? - initServer(); - const openApiDocument = generateOpenApi( await getContract(), - swaggerConf, + this.fastifyService.swaggerConf, { setOperationId: true }, ); - const app = fastify(fastifyConf) + const app = fastify(this.fastifyService.fastifyConf) .register(helmet, () => ({ contentSecurityPolicy: !( this.configurationService.isInt || @@ -116,8 +114,8 @@ export class AppService { .register(fastifySwagger, { transformObject: () => openApiDocument, }) - .register(fastifySwaggerUi, swaggerUiConf) - .register(apiRouter()) + .register(fastifySwaggerUi, this.fastifyService.swaggerUiConf) + .register(this.routerService.apiRouter()) .addHook('onRoute', (opts) => { if (opts.path === `${apiPrefix}/healthz`) { opts.logLevel = 'silent'; diff --git a/apps/server-nestjs/src/cpin-module/core/core.module.ts b/apps/server-nestjs/src/cpin-module/core/core.module.ts index 195a053b9..bdf1196bd 100644 --- a/apps/server-nestjs/src/cpin-module/core/core.module.ts +++ b/apps/server-nestjs/src/cpin-module/core/core.module.ts @@ -1,10 +1,20 @@ import { Module } from '@nestjs/common'; import { ConfigurationModule } from '../infrastructure/configuration/configuration.module'; +import { InfrastructureModule } from '../infrastructure/infrastructure.module'; import { AppService } from './app/app.service'; +import { FastifyService } from './fastify/fastify.service'; +import { RouterModule } from './router/router.module'; @Module({ - imports: [ConfigurationModule], - providers: [AppService], + imports: [ + ConfigurationModule, + RouterModule, + InfrastructureModule, + ], + providers: [ + AppService, + FastifyService + ], }) export class CoreModule {} diff --git a/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.spec.ts new file mode 100644 index 000000000..6c473f5b1 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FastifyService } from './fastify.service'; + +describe('FastifyService', () => { + let service: FastifyService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [FastifyService], + }).compile(); + + service = module.get(FastifyService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.ts b/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.ts new file mode 100644 index 000000000..4e98dc38b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.ts @@ -0,0 +1,62 @@ +import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; +import { loggerConfiguration } from '@/cpin-module/infrastructure/logger/logger.module'; +import { swaggerUiPath } from '@cpn-console/shared'; +import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui'; +import { Injectable } from '@nestjs/common'; +import type { generateOpenApi } from '@ts-rest/open-api'; +import type { FastifyServerOptions } from 'fastify'; +import { randomUUID } from 'node:crypto'; + +@Injectable() +export class FastifyService { + constructor(private readonly configurationService: ConfigurationService) { + this.fastifyConf = { + maxParamLength: 5000, + logger: + loggerConfiguration[this.configurationService.NODE_ENV] ?? + loggerConfiguration.production, + genReqId: () => randomUUID(), + }; + + this.swaggerConf = { + info: { + title: 'Console Cloud Pi Native', + description: 'API de gestion des ressources Cloud Pi Native.', + version: this.configurationService.appVersion, + }, + + externalDocs: this.externalDocs, + servers: [ + { + url: this.configurationService.keycloakRedirectUri, + }, + ], + }; + + this.swaggerUiConf = { + routePrefix: swaggerUiPath, + uiConfig: { + docExpansion: 'list', + deepLinking: false, + }, + initOAuth: { + clientId: this.configurationService.keycloakClientId, + clientSecret: this.configurationService.keycloakClientSecret, + realm: this.configurationService.keycloakRealm, + appName: 'Cloud Pi Native', + scopes: 'openid generic', + }, + }; + } + + fastifyConf!: FastifyServerOptions; + + externalDocs = { + description: 'External documentation.', + url: 'https://cloud-pi-native.fr', + }; + + swaggerConf: Parameters[1]; + + swaggerUiConf: FastifySwaggerUiOptions; +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts new file mode 100644 index 000000000..4022968e3 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminRoleRouterService } from './admin-role-router.service'; + +describe('AdminRoleRouterService', () => { + let service: AdminRoleRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminRoleRouterService], + }).compile(); + + service = module.get(AdminRoleRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts new file mode 100644 index 000000000..3a7117c93 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts @@ -0,0 +1,83 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from '@old-server/resources/admin-role/business'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class AdminRoleRouterService { + constructor(private readonly serverService: ServerService) {} + + adminRoleRouter() { + return this.serverService.serverInstance.router(adminRoleContract, { + async listAdminRoles() { + const body = await listRoles(); + + return { + status: 200, + body, + }; + }, + + async createAdminRole({ request: req, body }) { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await createRole(body); + + return { + status: 201, + body: resBody, + }; + }, + + async patchAdminRoles({ request: req, body }) { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await patchRoles(body); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 200, + body: resBody, + }; + }, + + async adminRoleMemberCounts({ request: req }) { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await countRolesMembers(); + + return { + status: 200, + body: resBody, + }; + }, + + async deleteAdminRole({ request: req, params }) { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await deleteRole(params.roleId); + + return { + status: 204, + body: resBody, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts new file mode 100644 index 000000000..2a759d155 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminTokenRouterService } from './admin-token-router.service'; + +describe('AdminTokenRouterService', () => { + let service: AdminTokenRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminTokenRouterService], + }).compile(); + + service = module.get(AdminTokenRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts new file mode 100644 index 000000000..3f365c71d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts @@ -0,0 +1,57 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + createToken, + deleteToken, + listTokens, +} from '@old-server/resources/admin-token/business'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class AdminTokenRouterService { + constructor(private readonly serverService: ServerService) {} + + adminTokenRouter() { + return this.serverService.serverInstance.router(adminTokenContract, { + listAdminTokens: async ({ request: req, query }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await listTokens(query); + + return { + status: 200, + body, + }; + }, + + createAdminToken: async ({ request: req, body: data }) => { + const perms = await authUser(req); + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await createToken(data); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + deleteAdminToken: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + await deleteToken(params.tokenId); + + return { + status: 204, + body: null, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts new file mode 100644 index 000000000..77b6d446a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ClusterRouterService } from './cluster-router.service'; + +describe('ClusterRouterService', () => { + let service: ClusterRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ClusterRouterService], + }).compile(); + + service = module.get(ClusterRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts new file mode 100644 index 000000000..0e9a89bce --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts @@ -0,0 +1,160 @@ +import type { AsyncReturnType } from '@cpn-console/shared'; +import { AdminAuthorized, clusterContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + createCluster, + deleteCluster, + getClusterAssociatedEnvironments, + getClusterDetails as getClusterDetailsBusiness, + getClusterUsage, + listClusters, + updateCluster, +} from '@old-server/resources/cluster/business'; +import '@old-server/types/index'; +import { authUser } from '@old-server/utils/controller'; +import { + ErrorResType, + Forbidden403, + Unauthorized401, +} from '@old-server/utils/errors'; + +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; + +@Injectable() +export class ClusterRouterService { + constructor(private readonly serverService: ServerService) {} + clusterRouter() { + return this.serverService.serverInstance.router(clusterContract, { + listClusters: async ({ request: req }) => { + const { adminPermissions, user } = await authUser(req); + + let body: AsyncReturnType = []; + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listClusters(); + } else if (user) { + body = await listClusters(user.id); + } + + return { + status: 200, + body, + }; + }, + + getClusterDetails: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const cluster = await getClusterDetailsBusiness(clusterId); + + return { + status: 200, + body: cluster, + }; + }, + + getClusterUsage: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const usage = await getClusterUsage(clusterId); + + return { + status: 200, + body: usage, + }; + }, + + createCluster: async ({ request: req, body: data }) => { + const { adminPermissions, user } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + const body = await createCluster(data, user.id, req.id); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + getClusterEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const clusterId = params.clusterId; + const environments = + await getClusterAssociatedEnvironments(clusterId); + + return { + status: 200, + body: environments, + }; + }, + + updateCluster: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + + const clusterId = params.clusterId; + const body = await updateCluster( + data, + clusterId, + user.id, + req.id, + ); + + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + deleteCluster: async ({ + request: req, + params, + query: { force }, + }) => { + const { user, adminPermissions, tokenId } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user?.id && !tokenId) + return new Unauthorized401( + 'Your identity has not been found', + ); + + const clusterId = params.clusterId; + const body = await deleteCluster({ + clusterId, + userId: user?.id, + requestId: req.id, + force, + }); + + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts new file mode 100644 index 000000000..062359821 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EnvironmentRouterService } from './environment-router.service'; + +describe('EnvironmentRouterService', () => { + let service: EnvironmentRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EnvironmentRouterService], + }).compile(); + + service = module.get(EnvironmentRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts new file mode 100644 index 000000000..b2757612d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts @@ -0,0 +1,155 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { ProjectAuthorized, environmentContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + checkEnvironmentCreate, + checkEnvironmentUpdate, + createEnvironment, + deleteEnvironment, + getProjectEnvironments, + updateEnvironment, +} from '@old-server/resources/environment/business'; +import { authUser } from '@old-server/utils/controller'; +import { + BadRequest400, + Forbidden403, + Internal500, + NotFound404, + Unauthorized401, +} from '@old-server/utils/errors'; + +@Injectable() +export class EnvironmentRouterService { + constructor(private readonly serverService: ServerService) {} + + environmentRouter() { + return this.serverService.serverInstance.router(environmentContract, { + listEnvironments: async ({ request: req, query }) => { + const projectId = query.projectId; + const perms = await authUser(req, { id: projectId }); + + const environments = ProjectAuthorized.ListEnvironments(perms) + ? await getProjectEnvironments(projectId) + : []; + + return { + status: 200, + body: environments, + }; + }, + + createEnvironment: async ({ request: req, body: requestBody }) => { + const projectId = requestBody.projectId; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const checkCreateResult = await checkEnvironmentCreate({ + ...requestBody, + }); + if (checkCreateResult.isError) + return new BadRequest400(checkCreateResult.error); + + const result = await createEnvironment({ + userId: perms.user.id, + projectId, + name: requestBody.name, + clusterId: requestBody.clusterId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + stageId: requestBody.stageId, + requestId: req.id, + }); + if (result.isError) { + return new Internal500(result.error); + } + return { + status: 201, + body: result.data, + }; + }, + + updateEnvironment: async ({ + request: req, + body: requestBody, + params, + }) => { + const { environmentId } = params; + const perms = await authUser(req, { environmentId }); + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!ProjectAuthorized.ListEnvironments(perms)) + return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const checkUpdateResult = await checkEnvironmentUpdate({ + environmentId, + ...requestBody, + }); + if (checkUpdateResult.isError) + return new BadRequest400(checkUpdateResult.error); + + const result = await updateEnvironment({ + user: perms.user, + environmentId, + cpu: requestBody.cpu, + gpu: requestBody.gpu, + memory: requestBody.memory, + requestId: req.id, + }); + if (result.isError) { + return new Internal500(result.error); + } + return { + status: 200, + body: result.data, + }; + }, + + deleteEnvironment: async ({ request: req, params }) => { + const { environmentId } = params; + const perms = await authUser(req, { environmentId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageEnvironments(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const result = await deleteEnvironment({ + userId: perms.user?.id, + environmentId, + requestId: req.id, + projectId: perms.projectId, + }); + if (result.isError) { + return new Internal500(result.error); + } + + return { + status: 204, + body: result.data, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts new file mode 100644 index 000000000..958c0af40 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LogRouterService } from './log-router.service'; + +describe('LogRouterService', () => { + let service: LogRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LogRouterService], + }).compile(); + + service = module.get(LogRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts new file mode 100644 index 000000000..44e5934b5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts @@ -0,0 +1,45 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { CleanLog, Log, XOR } from '@cpn-console/shared'; +import { AdminAuthorized, logContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { getLogs } from '@old-server/resources/log/business'; +import type { + UserProfile, + UserProjectProfile, +} from '@old-server/utils/controller'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class LogRouterService { + constructor(private readonly serverService: ServerService) {} + + logRouter() { + return this.serverService.serverInstance.router(logContract, { + // Récupérer des logs + getLogs: async ({ request: req, query }) => { + const perms: XOR = + query.projectId + ? await authUser(req, { id: query.projectId }) + : await authUser(req); + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { + if (!perms.projectPermissions) { + return new Forbidden403(); + } + query.clean = true; + } + + const [total, logs] = (await getLogs(query)) as [ + number, + unknown[], + ] as [number, Array]; + + return { + status: 200, + body: { total, logs }, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts new file mode 100644 index 000000000..63749a0dd --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProjectMemberRouterService } from './project-member-router.service'; + +describe('ProjectMemberRouterService', () => { + let service: ProjectMemberRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectMemberRouterService], + }).compile(); + + service = module.get(ProjectMemberRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts new file mode 100644 index 000000000..fecd58b7f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts @@ -0,0 +1,130 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { + AdminAuthorized, + ProjectAuthorized, + projectMemberContract, +} from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + addMember, + listMembers, + patchMembers, + removeMember, +} from '@old-server/resources/project-member/business'; +import { authUser } from '@old-server/utils/controller'; +import { + ErrorResType, + Forbidden403, + NotFound404, + Unauthorized401, +} from '@old-server/utils/errors'; + +@Injectable() +export class ProjectMemberRouterService { + constructor(private readonly serverService: ServerService) {} + + projectMemberRouter() { + return this.serverService.serverInstance.router(projectMemberContract, { + listMembers: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const body = await listMembers(projectId); + + return { + status: 200, + body, + }; + }, + + addMember: async ({ request: req, params, body }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageMembers(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await addMember( + projectId, + body, + perms.user.id, + req.id, + perms.projectOwnerId, + ); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 201, + body: resBody, + }; + }, + + patchMembers: async ({ request: req, params, body }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageMembers(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await patchMembers(projectId, body); + + return { + status: 200, + body: resBody, + }; + }, + + removeMember: async ({ request: req, params }) => { + const { projectId, userId } = params; + const perms = await authUser(req, { id: projectId }); + + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + if ( + !ProjectAuthorized.ManageMembers(perms) && + userId !== perms.user?.id + ) + return new Forbidden403(); + + const resBody = await removeMember(projectId, params.userId); + + return { + status: 200, + body: resBody, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts new file mode 100644 index 000000000..1fb57b0bb --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProjectRoleRouterService } from './project-role-router.service'; + +describe('ProjectRoleRouterService', () => { + let service: ProjectRoleRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectRoleRouterService], + }).compile(); + + service = module.get(ProjectRoleRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts new file mode 100644 index 000000000..73c612192 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts @@ -0,0 +1,136 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { + AdminAuthorized, + ProjectAuthorized, + projectRoleContract, +} from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + countRolesMembers, + createRole, + deleteRole, + listRoles, + patchRoles, +} from '@old-server/resources/project-role/business'; +import { authUser } from '@old-server/utils/controller'; +import { + ErrorResType, + Forbidden403, + NotFound404, +} from '@old-server/utils/errors'; + +@Injectable() +export class ProjectRoleRouterService { + constructor(private readonly serverService: ServerService) {} + + projectRoleRouter() { + return this.serverService.serverInstance.router(projectRoleContract, { + // Récupérer des projets + listProjectRoles: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const body = await listRoles(projectId); + + return { + status: 200, + body, + }; + }, + + createProjectRole: async ({ + request: req, + params: { projectId }, + body, + }) => { + const perms = await authUser(req, { id: projectId }); + + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await createRole(projectId, body); + + return { + status: 201, + body: resBody, + }; + }, + + patchProjectRoles: async ({ + request: req, + params: { projectId }, + body, + }) => { + const perms = await authUser(req, { id: projectId }); + + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await patchRoles(projectId, body); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 200, + body: resBody, + }; + }, + + projectRoleMemberCounts: async ({ request: req, params }) => { + const { projectId } = params; + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + + const resBody = await countRolesMembers(projectId); + + return { + status: 200, + body: resBody, + }; + }, + + deleteProjectRole: async ({ + request: req, + params: { projectId, roleId }, + }) => { + const perms = await authUser(req, { id: projectId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRoles(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const resBody = await deleteRole(roleId); + + return { + status: 204, + body: resBody, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts new file mode 100644 index 000000000..8ac556d0f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProjectRouterService } from './project-router.service'; + +describe('ProjectRouterService', () => { + let service: ProjectRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectRouterService], + }).compile(); + + service = module.get(ProjectRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts new file mode 100644 index 000000000..34154d9e4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts @@ -0,0 +1,251 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { AsyncReturnType } from '@cpn-console/shared'; +import { + AdminAuthorized, + ProjectAuthorized, + projectContract, +} from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + archiveProject, + bulkActionProject, + createProject, + generateProjectsData, + getProject, + getProjectSecrets, + listProjects, + replayHooks, + updateProject, +} from '@old-server/resources/project/business'; +import { authUser } from '@old-server/utils/controller'; +import { + BadRequest400, + ErrorResType, + Forbidden403, + NotFound404, + Unauthorized401, +} from '@old-server/utils/errors'; + +@Injectable() +export class ProjectRouterService { + constructor(private readonly serverService: ServerService) {} + + projectRouter() { + return this.serverService.serverInstance.router(projectContract, { + // Récupérer des projets + listProjects: async ({ request: req, query }) => { + const { adminPermissions, user } = await authUser(req); + let body: AsyncReturnType = []; + + if (adminPermissions && !user) { + // c'est donc un compte de service + query.filter = 'all'; + } + if ( + query.filter === 'all' && + !AdminAuthorized.isAdmin(adminPermissions) + ) { + return new BadRequest400( + "Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre 'all'", + ); + } + + body = await listProjects(query, user?.id); + + return { + status: 200, + body, + }; + }, + + // Récupérer les secrets d'un projet + getProjectSecrets: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.SeeSecrets(perms)) + return new Forbidden403(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await getProjectSecrets(projectId); + + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Créer un projet + createProject: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (perms.user?.type !== 'human') + return new Unauthorized401( + 'Cannot find requestor in database', + ); + const body = await createProject(data, perms.user, req.id); + + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Récuperer un seul projet + getProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.projectId) return new NotFound404(); + if (!isAdmin) { + if (!perms.projectPermissions) { + return new NotFound404(); + } + if (perms.projectStatus === 'archived') { + return new NotFound404(); + } + } + + const body = await getProject(projectId); + + return { + status: 200, + body, + }; + }, + + // Mettre à jour un projet + updateProject: async ({ request: req, params, body: data }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Cannot find requestor in database', + ); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + const isOwner = perms.projectOwnerId === perms.user.id; + + if (!perms.projectPermissions && !isAdmin) + return new NotFound404(); + if (!isAdmin) { + // filtrage des clés par niveau de permissions + delete data.locked; + if (!isOwner) { + delete data.ownerId; // impossible de toucher à cette clé + } + } + if (perms.projectLocked) { + if (!isAdmin) + return new Forbidden403('Le projet est verrouillé'); + if (data.locked !== false) + return new Forbidden403( + 'Veuillez déverrouiler le projet pour le mettre à jour', + ); + } + + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); + + const body = await updateProject( + data, + projectId, + perms.user, + req.id, + ); + + if (body instanceof ErrorResType) return body; + return { + status: 200, + body, + }; + }, + + // Reprovisionner un projet + replayHooksForProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.projectPermissions && !isAdmin) + return new NotFound404(); + if (!ProjectAuthorized.ReplayHooks(perms)) + return new Forbidden403(); + + const body = await replayHooks({ + projectId, + userId: perms.user?.id, + requestId: req.id, + }); + + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + + // Archiver un projet + archiveProject: async ({ request: req, params }) => { + const projectId = params.projectId; + const perms = await authUser(req, { id: projectId }); + const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); + + if (!perms.user) + return new Unauthorized401( + 'Cannot find requestor in database', + ); + if (!perms.projectPermissions && !isAdmin) + return new NotFound404(); + if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); + + const body = await archiveProject( + projectId, + perms.user, + req.id, + ); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + // Récupérer les données de tous les projets pour export + getProjectsData: async ({ request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + const body = await generateProjectsData(); + + return { + status: 200, + body, + }; + }, + + bulkActionProject: async ({ request: req, body }) => { + const perms = await authUser(req); + + if (!perms.user) + return new Unauthorized401( + 'Cannot find requestor in database', + ); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + await bulkActionProject(body, perms.user, req.id); + + return { + status: 202, + body: null, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts new file mode 100644 index 000000000..15b97798d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProjectServiceRouterService } from './project-service-router.service'; + +describe('ProjectServiceRouterService', () => { + let service: ProjectServiceRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectServiceRouterService], + }).compile(); + + service = module.get(ProjectServiceRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts new file mode 100644 index 000000000..09d363d52 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts @@ -0,0 +1,85 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { + AdminAuthorized, + ProjectAuthorized, + projectServiceContract, +} from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + getProjectServices, + updateProjectServices, +} from '@old-server/resources/project-service/business'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403, NotFound404 } from '@old-server/utils/errors'; + +@Injectable() +export class ProjectServiceRouterService { + constructor(private readonly serverService: ServerService) {} + + projectServiceRouter() { + return this.serverService.serverInstance.router( + projectServiceContract, + { + // Récupérer les services d'un projet + getServices: async ({ + request: req, + params: { projectId }, + query, + }) => { + const perms = await authUser(req, { id: projectId }); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if ( + !AdminAuthorized.isAdmin(perms.adminPermissions) && + query.permissionTarget === 'admin' + ) + return new Forbidden403( + 'Vous ne pouvez pas demander les paramètres admin', + ); + + const body = await getProjectServices( + projectId, + query.permissionTarget, + ); + + return { + status: 200, + body, + }; + }, + + updateProjectServices: async ({ + request: req, + params: { projectId }, + body, + }) => { + const perms = await authUser(req, { id: projectId }); + if (!ProjectAuthorized.Manage(perms)) + return new NotFound404(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + + const allowedRoles: Array<'user' | 'admin'> = + AdminAuthorized.isAdmin(perms.adminPermissions) + ? ['user', 'admin'] + : ['user']; + + const resBody = await updateProjectServices( + projectId, + body, + allowedRoles, + ); + return { + status: 204, + body: resBody, + }; + }, + }, + ); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts new file mode 100644 index 000000000..b27de5a4c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RepositoryRouterService } from './repository-router.service'; + +describe('RepositoryRouterService', () => { + let service: RepositoryRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RepositoryRouterService], + }).compile(); + + service = module.get(RepositoryRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts new file mode 100644 index 000000000..d5acd1562 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts @@ -0,0 +1,199 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { + AdminAuthorized, + ProjectAuthorized, + fakeToken, + repositoryContract, +} from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + createRepository, + deleteRepository, + getProjectRepositories, + syncRepository, + updateRepository, +} from '@old-server/resources/repository/business'; +import { authUser } from '@old-server/utils/controller'; +import { + ErrorResType, + Forbidden403, + NotFound404, + Unauthorized401, +} from '@old-server/utils/errors'; +import { filterObjectByKeys } from '@old-server/utils/queries-tools'; + +@Injectable() +export class RepositoryRouterService { + constructor(private readonly serverService: ServerService) {} + + repositoryRouter() { + return this.serverService.serverInstance.router(repositoryContract, { + // Récupérer tous les repositories d'un projet + listRepositories: async ({ request: req, query }) => { + const projectId = query.projectId; + const perms = await authUser(req, { id: projectId }); + + const body = ProjectAuthorized.ListRepositories(perms) + ? await getProjectRepositories(projectId) + : []; + + return { + status: 200, + body, + }; + }, + + // Synchroniser un repository + syncRepository: async ({ request: req, params, body }) => { + const { repositoryId } = params; + const perms = await authUser(req, { repositoryId }); + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const { syncAllBranches, branchName } = body; + + const resBody = await syncRepository({ + repositoryId, + userId: perms.user.id, + branchName, + requestId: req.id, + syncAllBranches, + }); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 204, + body: resBody, + }; + }, + + // Créer un repository + createRepository: async ({ request: req, body: data }) => { + const projectId = data.projectId; + const perms = await authUser(req, { id: projectId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await createRepository({ + data, + userId: perms.user.id, + requestId: req.id, + }); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Mettre à jour un repository + updateRepository: async ({ request: req, params, body }) => { + const repositoryId = params.repositoryId; + const perms = await authUser(req, { repositoryId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if ( + !perms.projectPermissions && + !AdminAuthorized.isAdmin(perms.adminPermissions) + ) + return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const keysAllowedForUpdate = [ + 'externalRepoUrl', + 'isPrivate', + 'externalToken', + 'externalUserName', + 'isInfra', + 'deployRevision', + 'deployPath', + 'helmValuesFiles', + ]; + const data = filterObjectByKeys(body, keysAllowedForUpdate); + + if (data.externalToken === fakeToken) { + delete data.externalToken; + } + + if (data.isPrivate === false) { + delete data.externalToken; + delete data.externalUserName; + } + + const resBody = await updateRepository({ + repositoryId, + data, + userId: perms.user.id, + requestId: req.id, + }); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 200, + body: resBody, + }; + }, + + // Supprimer un repository + deleteRepository: async ({ request: req, params }) => { + const repositoryId = params.repositoryId; + const perms = await authUser(req, { repositoryId }); + + if (!perms.user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + if (!perms.projectPermissions) return new NotFound404(); + if (!ProjectAuthorized.ManageRepositories(perms)) + return new Forbidden403(); + if (perms.projectLocked) + return new Forbidden403('Le projet est verrouillé'); + if (perms.projectStatus === 'archived') + return new Forbidden403('Le projet est archivé'); + + const body = await deleteRepository({ + repositoryId, + userId: perms.user.id, + requestId: req.id, + projectId: perms.projectId, + }); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/router.module.ts b/apps/server-nestjs/src/cpin-module/core/router/router.module.ts new file mode 100644 index 000000000..c5e0f9bf7 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/router.module.ts @@ -0,0 +1,52 @@ +import { ConfigurationModule } from '@/cpin-module/infrastructure/configuration/configuration.module'; +import { InfrastructureModule } from '@/cpin-module/infrastructure/infrastructure.module'; +import { Module } from '@nestjs/common'; + +import { AdminRoleRouterService } from './admin-role-router/admin-role-router.service'; +import { AdminTokenRouterService } from './admin-token-router/admin-token-router.service'; +import { ClusterRouterService } from './cluster-router/cluster-router.service'; +import { EnvironmentRouterService } from './environment-router/environment-router.service'; +import { LogRouterService } from './log-router/log-router.service'; +import { ProjectMemberRouterService } from './project-member-router/project-member-router.service'; +import { ProjectRoleRouterService } from './project-role-router/project-role-router.service'; +import { ProjectRouterService } from './project-router/project-router.service'; +import { ProjectServiceRouterService } from './project-service-router/project-service-router.service'; +import { RepositoryRouterService } from './repository-router/repository-router.service'; +import { RouterService } from './router.service'; +import { ServiceChainRouterService } from './service-chain-router/service-chain-router.service'; +import { ServiceMonitorRouterService } from './service-monitor-router/service-monitor-router.service'; +import { StageRouterService } from './stage-router/stage-router.service'; +import { SystemConfigRouterService } from './system-config-router/system-config-router.service'; +import { SystemRouterService } from './system-router/system-router.service'; +import { SystemSettingsRouterService } from './system-settings-router/system-settings-router.service'; +import { UserRouterService } from './user-router/user-router.service'; +import { UserTokensRouterService } from './user-tokens-router/user-tokens-router.service'; +import { ZoneRouterService } from './zone-router/zone-router.service'; + +@Module({ + imports: [InfrastructureModule, ConfigurationModule], + providers: [ + AdminRoleRouterService, + AdminTokenRouterService, + ClusterRouterService, + EnvironmentRouterService, + LogRouterService, + ProjectMemberRouterService, + ProjectRoleRouterService, + ProjectRouterService, + ProjectServiceRouterService, + RepositoryRouterService, + RouterService, + ServiceChainRouterService, + ServiceMonitorRouterService, + StageRouterService, + SystemConfigRouterService, + SystemRouterService, + SystemSettingsRouterService, + UserRouterService, + UserTokensRouterService, + ZoneRouterService, + ], + exports: [RouterService], +}) +export class RouterModule {} diff --git a/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts new file mode 100644 index 000000000..c35fac5d3 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RouterService } from './router.service'; + +describe('RouterService', () => { + let service: RouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RouterService], + }).compile(); + + service = module.get(RouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/router.service.ts new file mode 100644 index 000000000..9b58fcb82 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/router.service.ts @@ -0,0 +1,171 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { Injectable } from '@nestjs/common'; +import type { FastifyInstance } from 'fastify'; + +import { AdminRoleRouterService } from './admin-role-router/admin-role-router.service'; +import { AdminTokenRouterService } from './admin-token-router/admin-token-router.service'; +import { ClusterRouterService } from './cluster-router/cluster-router.service'; +import { EnvironmentRouterService } from './environment-router/environment-router.service'; +import { LogRouterService } from './log-router/log-router.service'; +import { ProjectMemberRouterService } from './project-member-router/project-member-router.service'; +import { ProjectRoleRouterService } from './project-role-router/project-role-router.service'; +import { ProjectRouterService } from './project-router/project-router.service'; +import { ProjectServiceRouterService } from './project-service-router/project-service-router.service'; +import { RepositoryRouterService } from './repository-router/repository-router.service'; +import { ServiceChainRouterService } from './service-chain-router/service-chain-router.service'; +import { ServiceMonitorRouterService } from './service-monitor-router/service-monitor-router.service'; +import { StageRouterService } from './stage-router/stage-router.service'; +import { SystemConfigRouterService } from './system-config-router/system-config-router.service'; +import { SystemRouterService } from './system-router/system-router.service'; +import { SystemSettingsRouterService } from './system-settings-router/system-settings-router.service'; +import { UserRouterService } from './user-router/user-router.service'; +import { UserTokensRouterService } from './user-tokens-router/user-tokens-router.service'; +import { ZoneRouterService } from './zone-router/zone-router.service'; + +@Injectable() +export class RouterService { + constructor( + private readonly serverService: ServerService, + private readonly adminRoleRouterService: AdminRoleRouterService, + private readonly adminTokenRouterService: AdminTokenRouterService, + private readonly clusterRouterService: ClusterRouterService, + private readonly environmentRouterService: EnvironmentRouterService, + private readonly logRouterService: LogRouterService, + private readonly projectMemberRouterService: ProjectMemberRouterService, + private readonly projectRoleRouterService: ProjectRoleRouterService, + private readonly projectRouterService: ProjectRouterService, + private readonly projectServiceRouterService: ProjectServiceRouterService, + private readonly repositoryRouterService: RepositoryRouterService, + private readonly serviceChainRouterService: ServiceChainRouterService, + private readonly serviceMonitorRouterService: ServiceMonitorRouterService, + private readonly stageRouterService: StageRouterService, + private readonly systemConfigRouterService: SystemConfigRouterService, + private readonly systemRouterService: SystemRouterService, + private readonly systemSettingsRouterService: SystemSettingsRouterService, + private readonly userRouterService: UserRouterService, + private readonly userTokensRouterService: UserTokensRouterService, + private readonly zoneRouterService: ZoneRouterService, + ) {} + // relax validation schema if NO_VALIDATION env var is set to true. + // /!\ It can lead to security leaks !!!! + validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' }; + + apiRouter() { + return async (app: FastifyInstance) => { + await app.register( + this.serverService.serverInstance.plugin( + this.adminRoleRouterService.adminRoleRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.adminTokenRouterService.adminTokenRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.clusterRouterService.clusterRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.serviceChainRouterService.serviceChainRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.environmentRouterService.environmentRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.logRouterService.logRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.userTokensRouterService.userTokensRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.projectRouterService.projectRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.projectMemberRouterService.projectMemberRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.projectRoleRouterService.projectRoleRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.projectServiceRouterService.projectServiceRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.repositoryRouterService.repositoryRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.serviceMonitorRouterService.serviceMonitorRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.systemConfigRouterService.systemConfigRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.stageRouterService.stageRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.systemRouterService.systemRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.systemSettingsRouterService.systemSettingsRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.userRouterService.userRouter(), + ), + this.validateTrue, + ); + await app.register( + this.serverService.serverInstance.plugin( + this.zoneRouterService.zoneRouter(), + ), + this.validateTrue, + ); + }; + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts new file mode 100644 index 000000000..70902e4d4 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ServiceChainRouterService } from './service-chain-router.service'; + +describe('ServiceChainRouterService', () => { + let service: ServiceChainRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ServiceChainRouterService], + }).compile(); + + service = module.get(ServiceChainRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts new file mode 100644 index 000000000..0b9cbab27 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts @@ -0,0 +1,96 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { AsyncReturnType } from '@cpn-console/shared'; +import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + getServiceChainDetails as getServiceChainDetailsBusiness, + getServiceChainFlows as getServiceChainFlowsBusiness, + listServiceChains as listServiceChainsBusiness, + retryServiceChain as retryServiceChainBusiness, + validateServiceChain as validateServiceChainBusiness, +} from '@old-server/resources/service-chain/business'; +import '@old-server/types/index'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class ServiceChainRouterService { + constructor(private readonly serverService: ServerService) {} + + serviceChainRouter() { + return this.serverService.serverInstance.router(serviceChainContract, { + listServiceChains: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + + let body: AsyncReturnType = + []; + if (AdminAuthorized.isAdmin(adminPermissions)) { + body = await listServiceChainsBusiness(); + } + + return { + status: 200, + body, + }; + }, + + getServiceChainDetails: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + const serviceChainDetails = + await getServiceChainDetailsBusiness(serviceChainId); + + return { + status: 200, + body: serviceChainDetails, + }; + }, + + retryServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + await retryServiceChainBusiness(serviceChainId); + + return { + status: 204, + body: null, + }; + }, + + validateServiceChain: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.validationId; + await validateServiceChainBusiness(serviceChainId); + + return { + status: 204, + body: null, + }; + }, + + getServiceChainFlows: async ({ params, request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const serviceChainId = params.serviceChainId; + const serviceChainFlows = + await getServiceChainFlowsBusiness(serviceChainId); + + return { + status: 200, + body: serviceChainFlows, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts new file mode 100644 index 000000000..d1b53b2a6 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ServiceMonitorRouterService } from './service-monitor-router.service'; + +describe('ServiceMonitorRouterService', () => { + let service: ServiceMonitorRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ServiceMonitorRouterService], + }).compile(); + + service = module.get(ServiceMonitorRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts new file mode 100644 index 000000000..7a9846feb --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts @@ -0,0 +1,54 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { AdminAuthorized, serviceContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + checkServicesHealth, + refreshServicesHealth, +} from '@old-server/resources/service-monitor/business'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class ServiceMonitorRouterService { + constructor(private readonly serverService: ServerService) {} + + serviceMonitorRouter() { + return this.serverService.serverInstance.router(serviceContract, { + getServiceHealth: async () => { + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + + getCompleteServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + + refreshServiceHealth: async ({ request: req }) => { + const { adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + + await refreshServicesHealth(); + const serviceData = checkServicesHealth(); + + return { + status: 200, + body: serviceData, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts new file mode 100644 index 000000000..aec34bee9 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { StageRouterService } from './stage-router.service'; + +describe('StageRouterService', () => { + let service: StageRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [StageRouterService], + }).compile(); + + service = module.get(StageRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts new file mode 100644 index 000000000..43f1ac65f --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts @@ -0,0 +1,95 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { AdminAuthorized, stageContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + createStage, + deleteStage, + getStageAssociatedEnvironments, + listStages, + updateStage, +} from '@old-server/resources/stage/business'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class StageRouterService { + constructor(private readonly serverService: ServerService) {} + stageRouter() { + return this.serverService.serverInstance.router(stageContract, { + // Récupérer les types d'environnement disponibles + listStages: async () => { + const body = await listStages(); + + return { + status: 200, + body, + }; + }, + + // Récupérer les environnements associés au stage + getStageEnvironments: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + const body = await getStageAssociatedEnvironments(stageId); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Créer un stage + createStage: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const body = await createStage(data); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + // Modifier une association stage / clusters + updateStage: async ({ request: req, params, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + + const body = await updateStage(stageId, data); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + // Supprimer un stage + deleteStage: async ({ request: req, params }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const stageId = params.stageId; + + const body = await deleteStage(stageId); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts new file mode 100644 index 000000000..4691aa309 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SystemConfigRouterService } from './system-config-router.service'; + +describe('SystemConfigRouterService', () => { + let service: SystemConfigRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SystemConfigRouterService], + }).compile(); + + service = module.get(SystemConfigRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts new file mode 100644 index 000000000..b0a0dd74b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts @@ -0,0 +1,46 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + getPluginsConfig, + updatePluginConfig, +} from '@old-server/resources/system/config/business'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class SystemConfigRouterService { + constructor(private readonly serverService: ServerService) {} + + systemConfigRouter() { + return this.serverService.serverInstance.router(systemPluginContract, { + // Récupérer les configurations plugins + getPluginsConfig: async ({ request: req }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const services = await getPluginsConfig(); + + return { + status: 200, + body: services, + }; + }, + // Mettre à jour les configurations plugins + updatePluginsConfig: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const resBody = await updatePluginConfig(body); + if (resBody instanceof ErrorResType) return resBody; + + return { + status: 204, + body: resBody, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts new file mode 100644 index 000000000..7ddc7e4ee --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SystemRouterService } from './system-router.service'; + +describe('SystemRouterService', () => { + let service: SystemRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SystemRouterService], + }).compile(); + + service = module.get(SystemRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts new file mode 100644 index 000000000..4d1cbaddf --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts @@ -0,0 +1,30 @@ +import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { systemContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class SystemRouterService { + constructor( + private readonly configurationService: ConfigurationService, + private readonly serverService: ServerService, + ) {} + + systemRouter() { + return this.serverService.serverInstance.router(systemContract, { + getVersion: async () => ({ + status: 200, + body: { + version: this.configurationService.appVersion, + }, + }), + + getHealth: async () => ({ + status: 200, + body: { + status: 'OK', + }, + }), + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts new file mode 100644 index 000000000..be353261a --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SystemSettingsRouterService } from './system-settings-router.service'; + +describe('SystemSettingsRouterService', () => { + let service: SystemSettingsRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SystemSettingsRouterService], + }).compile(); + + service = module.get(SystemSettingsRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts new file mode 100644 index 000000000..0859f1770 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts @@ -0,0 +1,43 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + getSystemSettings, + upsertSystemSetting, +} from '@old-server/resources/system/settings/business'; +import { authUser } from '@old-server/utils/controller'; +import { Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class SystemSettingsRouterService { + constructor(private readonly serverService: ServerService) {} + + systemSettingsRouter() { + return this.serverService.serverInstance.router( + systemSettingsContract, + { + listSystemSettings: async ({ query }) => { + const systemSettings = await getSystemSettings(query.key); + + return { + status: 200, + body: systemSettings, + }; + }, + + upsertSystemSetting: async ({ request: req, body: data }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const systemSetting = await upsertSystemSetting(data); + + return { + status: 201, + body: systemSetting, + }; + }, + }, + ); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts new file mode 100644 index 000000000..a0fb5beec --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserRouterService } from './user-router.service'; + +describe('UserRouterService', () => { + let service: UserRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserRouterService], + }).compile(); + + service = module.get(UserRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts new file mode 100644 index 000000000..7094ed1fd --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts @@ -0,0 +1,78 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { AdminAuthorized, userContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + getMatchingUsers, + getUsers, + logViaSession, + patchUsers, +} from '@old-server/resources/user/business'; +import '@old-server/types/index'; +import { authUser } from '@old-server/utils/controller'; +import { + ErrorResType, + Forbidden403, + Unauthorized401, +} from '@old-server/utils/errors'; + +@Injectable() +export class UserRouterService { + constructor(private readonly serverService: ServerService) {} + + userRouter() { + return this.serverService.serverInstance.router(userContract, { + getMatchingUsers: async ({ query }) => { + const usersMatching = await getMatchingUsers(query); + + return { + status: 200, + body: usersMatching, + }; + }, + + auth: async ({ request: req }) => { + const user = req.session.user; + + if (!user) return new Unauthorized401(); + + const { user: body } = await logViaSession(user); + + return { + status: 200, + body, + }; + }, + + getAllUsers: async ({ + request: req, + query: { relationType, ...query }, + }) => { + const perms = await authUser(req); + + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const body = await getUsers(query, relationType); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + patchUsers: async ({ request: req, body }) => { + const perms = await authUser(req); + if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + return new Forbidden403(); + + const users = await patchUsers(body); + + return { + status: 200, + body: users, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts new file mode 100644 index 000000000..28bc6575d --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserTokensRouterService } from './user-tokens-router.service'; + +describe('UserTokensRouterService', () => { + let service: UserTokensRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserTokensRouterService], + }).compile(); + + service = module.get(UserTokensRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts new file mode 100644 index 000000000..8bb1be8e7 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts @@ -0,0 +1,67 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { personalAccessTokenContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + createToken, + deleteToken, + listTokens, +} from '@old-server/resources/user/tokens/business'; +// @TODO: Nécessaire ? +// import '@old-server/types/index'; +import { authUser } from '@old-server/utils/controller'; +import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; + +@Injectable() +export class UserTokensRouterService { + constructor(private readonly serverService: ServerService) {} + + userTokensRouter() { + return this.serverService.serverInstance.router( + personalAccessTokenContract, + { + listPersonalAccessTokens: async ({ request: req }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + const body = await listTokens(perms.user.id); + + return { + status: 200, + body, + }; + }, + + createPersonalAccessToken: async ({ + request: req, + body: data, + }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + const body = await createToken(data, perms.user.id); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + deletePersonalAccessToken: async ({ request: req, params }) => { + const perms = await authUser(req); + + if (!perms.user?.id || perms.user?.type !== 'human') + return new Forbidden403(); + await deleteToken(params.tokenId, perms.user.id); + + return { + status: 204, + body: null, + }; + }, + }, + ); + } +} diff --git a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts new file mode 100644 index 000000000..055c9bd4b --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ZoneRouterService } from './zone-router.service'; + +describe('ZoneRouterService', () => { + let service: ZoneRouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ZoneRouterService], + }).compile(); + + service = module.get(ZoneRouterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts new file mode 100644 index 000000000..c2545e7f8 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts @@ -0,0 +1,90 @@ +import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import { AdminAuthorized, zoneContract } from '@cpn-console/shared'; +import { Injectable } from '@nestjs/common'; +import { + createZone, + deleteZone, + listZones, + updateZone, +} from '@old-server/resources/zone/business'; +import { authUser } from '@old-server/utils/controller'; +import { + ErrorResType, + Forbidden403, + Unauthorized401, +} from '@old-server/utils/errors'; + +@Injectable() +export class ZoneRouterService { + constructor(private readonly serverService: ServerService) {} + + zoneRouter() { + return this.serverService.serverInstance.router(zoneContract, { + listZones: async () => { + const zones = await listZones(); + + return { + status: 200, + body: zones, + }; + }, + + createZone: async ({ request: req, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + + const body = await createZone(data, user.id, req.id); + if (body instanceof ErrorResType) return body; + + return { + status: 201, + body, + }; + }, + + updateZone: async ({ request: req, params, body: data }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + + const zoneId = params.zoneId; + + const body = await updateZone(zoneId, data, user.id, req.id); + if (body instanceof ErrorResType) return body; + + return { + status: 200, + body, + }; + }, + + deleteZone: async ({ request: req, params }) => { + const { user, adminPermissions } = await authUser(req); + if (!AdminAuthorized.isAdmin(adminPermissions)) + return new Forbidden403(); + if (!user) + return new Unauthorized401( + 'Require to be requested from user not api key', + ); + const zoneId = params.zoneId; + + const body = await deleteZone(zoneId, user.id, req.id); + if (body instanceof ErrorResType) return body; + + return { + status: 204, + body, + }; + }, + }); + } +} diff --git a/apps/server-nestjs/src/cpin-module/cpin.module.ts b/apps/server-nestjs/src/cpin-module/cpin.module.ts index 804f4b84f..2daee31d0 100644 --- a/apps/server-nestjs/src/cpin-module/cpin.module.ts +++ b/apps/server-nestjs/src/cpin-module/cpin.module.ts @@ -8,8 +8,6 @@ import { InfrastructureModule } from './infrastructure/infrastructure.module'; // It it means to be empty in the future, by extracting from it // as many modules as possible ! @Module({ - controllers: [], - providers: [], imports: [ ApplicationInitializationModule, CoreModule, diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts index b0a10de4c..4423714f5 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts @@ -44,8 +44,4 @@ export class ConfigurationService { ? 'development' : 'production'; - // server tuning - parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT - ? Number(process.env.PARALLEL_BULK_LIMIT) - : 5; } diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts index 1c61bd1e4..4837dd4bf 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts @@ -6,10 +6,9 @@ import { ConfigurationService } from '../configuration/configuration.service'; @Injectable() export class DatabaseService { - constructor( - private readonly configurationService: ConfigurationService, - private readonly loggerService = new Logger(DatabaseService.name), - ) { + private readonly loggerService = new Logger(DatabaseService.name); + + constructor(private readonly configurationService: ConfigurationService) { this.DELAY_BEFORE_RETRY = this.configurationService.isTest || this.configurationService.isCI ? 1000 diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts index e4eb179f4..86cd80c5c 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts @@ -5,9 +5,21 @@ import { DatabaseService } from './database/database.service'; import { FastifyService } from './fastify/fastify.service'; import { HttpClientService } from './http-client/http-client.service'; import { LoggerModule } from './logger/logger.module'; +import { ServerService } from './server/server.service'; @Module({ - providers: [DatabaseService, HttpClientService, FastifyService], + providers: [ + DatabaseService, + HttpClientService, + FastifyService, + ServerService, + ], imports: [LoggerModule, ConfigurationModule], + exports: [ + DatabaseService, + HttpClientService, + FastifyService, + ServerService, + ], }) export class InfrastructureModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.spec.ts new file mode 100644 index 000000000..9df92ef57 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ServerService } from './server.service'; + +describe('ServerService', () => { + let service: ServerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ServerService], + }).compile(); + + service = module.get(ServerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.ts new file mode 100644 index 000000000..d82590df5 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { initServer } from '@ts-rest/fastify'; + +@Injectable() +//@TODO is this still necessary ? +export class ServerService { + serverInstance!: any; + + constructor() { + this.serverInstance = initServer(); + } + +} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts index 268ffb696..9a5a81217 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts @@ -1,74 +1,74 @@ -import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared' -import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' +// import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared' +// import { + // countRolesMembers, + // createRole, + // deleteRole, + // listRoles, + // patchRoles, +// } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' -export function adminRoleRouter() { - return serverInstance.router(adminRoleContract, { - // Récupérer des projets - listAdminRoles: async () => { - const body = await listRoles() +// export function adminRoleRouter() { + // return serverInstance.router(adminRoleContract, { + // // Récupérer des projets + // listAdminRoles: async () => { + // const body = await listRoles() - return { - status: 200, - body, - } - }, + // return { + // status: 200, + // body, + // } + // }, - createAdminRole: async ({ request: req, body }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // createAdminRole: async ({ request: req, body }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const resBody = await createRole(body) + // const resBody = await createRole(body) - return { - status: 201, - body: resBody, - } - }, + // return { + // status: 201, + // body: resBody, + // } + // }, - patchAdminRoles: async ({ request: req, body }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // patchAdminRoles: async ({ request: req, body }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const resBody = await patchRoles(body) - if (resBody instanceof ErrorResType) return resBody + // const resBody = await patchRoles(body) + // if (resBody instanceof ErrorResType) return resBody - return { - status: 200, - body: resBody, - } - }, + // return { + // status: 200, + // body: resBody, + // } + // }, - adminRoleMemberCounts: async ({ request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // adminRoleMemberCounts: async ({ request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const resBody = await countRolesMembers() + // const resBody = await countRolesMembers() - return { - status: 200, - body: resBody, - } - }, + // return { + // status: 200, + // body: resBody, + // } + // }, - deleteAdminRole: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // deleteAdminRole: async ({ request: req, params }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const resBody = await deleteRole(params.roleId) + // const resBody = await deleteRole(params.roleId) - return { - status: 204, - body: resBody, - } - }, - }) -} + // return { + // status: 204, + // body: resBody, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts index 301b465d7..3ee61ed10 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts @@ -1,44 +1,44 @@ -import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared' -import { serverInstance } from '../../app' -import { createToken, deleteToken, listTokens } from './business' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' +// import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared' +// import { serverInstance } from '../../app' +// import { createToken, deleteToken, listTokens } from './business' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' -export function adminTokenRouter() { - return serverInstance.router(adminTokenContract, { - listAdminTokens: async ({ request: req, query }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const body = await listTokens(query) +// export function adminTokenRouter() { + // return serverInstance.router(adminTokenContract, { + // listAdminTokens: async ({ request: req, query }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // const body = await listTokens(query) - return { - status: 200, - body, - } - }, + // return { + // status: 200, + // body, + // } + // }, - createAdminToken: async ({ request: req, body: data }) => { - const perms = await authUser(req) + // createAdminToken: async ({ request: req, body: data }) => { + // const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const body = await createToken(data) - if (body instanceof ErrorResType) return body + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // const body = await createToken(data) + // if (body instanceof ErrorResType) return body - return { - status: 201, - body, - } - }, + // return { + // status: 201, + // body, + // } + // }, - deleteAdminToken: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - await deleteToken(params.tokenId) + // deleteAdminToken: async ({ request: req, params }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // await deleteToken(params.tokenId) - return { - status: 204, - body: null, - } - }, - }) -} + // return { + // status: 204, + // body: null, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts index 9824fe104..211f753b5 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts @@ -1,125 +1,125 @@ -import type { AsyncReturnType } from '@cpn-console/shared' -import { AdminAuthorized, clusterContract } from '@cpn-console/shared' -import { - createCluster, - deleteCluster, - getClusterAssociatedEnvironments, - getClusterDetails as getClusterDetailsBusiness, - getClusterUsage, - listClusters, - updateCluster, -} from './business' -import '@old-server/types/index' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' - -export function clusterRouter() { - return serverInstance.router(clusterContract, { - listClusters: async ({ request: req }) => { - const { adminPermissions, user } = await authUser(req) - - let body: AsyncReturnType = [] - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listClusters() - } else if (user) { - body = await listClusters(user.id) - } - - return { - status: 200, - body, - } - }, - - getClusterDetails: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const clusterId = params.clusterId - const cluster = await getClusterDetailsBusiness(clusterId) - - return { - status: 200, - body: cluster, - } - }, - - getClusterUsage: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const clusterId = params.clusterId - const usage = await getClusterUsage(clusterId) - - return { - status: 200, - body: usage, - } - }, - - createCluster: async ({ request: req, body: data }) => { - const { adminPermissions, user } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - - if (!user) return new Unauthorized401('Require to be requested from user not api key') - const body = await createCluster(data, user.id, req.id) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - getClusterEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const clusterId = params.clusterId - const environments = await getClusterAssociatedEnvironments(clusterId) - - return { - status: 200, - body: environments, - } - }, - - updateCluster: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user) return new Unauthorized401('Require to be requested from user not api key') - - const clusterId = params.clusterId - const body = await updateCluster(data, clusterId, user.id, req.id) - - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - deleteCluster: async ({ request: req, params, query: { force } }) => { - const { user, adminPermissions, tokenId } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user?.id && !tokenId) return new Unauthorized401('Your identity has not been found') - - const clusterId = params.clusterId - const body = await deleteCluster({ - clusterId, - userId: user?.id, - requestId: req.id, - force, - }) - - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - }) -} +// import type { AsyncReturnType } from '@cpn-console/shared' +// import { AdminAuthorized, clusterContract } from '@cpn-console/shared' +// import { + // createCluster, + // deleteCluster, + // getClusterAssociatedEnvironments, + // getClusterDetails as getClusterDetailsBusiness, + // getClusterUsage, + // listClusters, + // updateCluster, +// } from './business' +// import '@old-server/types/index' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' + +// export function clusterRouter() { + // return serverInstance.router(clusterContract, { + // listClusters: async ({ request: req }) => { + // const { adminPermissions, user } = await authUser(req) + + // let body: AsyncReturnType = [] + // if (AdminAuthorized.isAdmin(adminPermissions)) { + // body = await listClusters() + // } else if (user) { + // body = await listClusters(user.id) + // } + + // return { + // status: 200, + // body, + // } + // }, + + // getClusterDetails: async ({ params, request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const clusterId = params.clusterId + // const cluster = await getClusterDetailsBusiness(clusterId) + + // return { + // status: 200, + // body: cluster, + // } + // }, + + // getClusterUsage: async ({ params, request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const clusterId = params.clusterId + // const usage = await getClusterUsage(clusterId) + + // return { + // status: 200, + // body: usage, + // } + // }, + + // createCluster: async ({ request: req, body: data }) => { + // const { adminPermissions, user } = await authUser(req) + // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + + // if (!user) return new Unauthorized401('Require to be requested from user not api key') + // const body = await createCluster(data, user.id, req.id) + // if (body instanceof ErrorResType) return body + + // return { + // status: 201, + // body, + // } + // }, + + // getClusterEnvironments: async ({ request: req, params }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const clusterId = params.clusterId + // const environments = await getClusterAssociatedEnvironments(clusterId) + + // return { + // status: 200, + // body: environments, + // } + // }, + + // updateCluster: async ({ request: req, params, body: data }) => { + // const { user, adminPermissions } = await authUser(req) + // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + // if (!user) return new Unauthorized401('Require to be requested from user not api key') + + // const clusterId = params.clusterId + // const body = await updateCluster(data, clusterId, user.id, req.id) + + // if (body instanceof ErrorResType) return body + + // return { + // status: 200, + // body, + // } + // }, + + // deleteCluster: async ({ request: req, params, query: { force } }) => { + // const { user, adminPermissions, tokenId } = await authUser(req) + // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + // if (!user?.id && !tokenId) return new Unauthorized401('Your identity has not been found') + + // const clusterId = params.clusterId + // const body = await deleteCluster({ + // clusterId, + // userId: user?.id, + // requestId: req.id, + // force, + // }) + + // if (body instanceof ErrorResType) return body + + // return { + // status: 204, + // body, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts index 699f63471..1fb9950a1 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts @@ -1,109 +1,109 @@ -import { ProjectAuthorized, environmentContract } from '@cpn-console/shared' -import { checkEnvironmentCreate, checkEnvironmentUpdate, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401 } from '@old-server/utils/errors' +// import { ProjectAuthorized, environmentContract } from '@cpn-console/shared' +// import { checkEnvironmentCreate, checkEnvironmentUpdate, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401 } from '@old-server/utils/errors' -export function environmentRouter() { - return serverInstance.router(environmentContract, { - listEnvironments: async ({ request: req, query }) => { - const projectId = query.projectId - const perms = await authUser(req, { id: projectId }) +// export function environmentRouter() { + // return serverInstance.router(environmentContract, { + // listEnvironments: async ({ request: req, query }) => { + // const projectId = query.projectId + // const perms = await authUser(req, { id: projectId }) - const environments = ProjectAuthorized.ListEnvironments(perms) - ? await getProjectEnvironments(projectId) - : [] + // const environments = ProjectAuthorized.ListEnvironments(perms) + // ? await getProjectEnvironments(projectId) + // : [] - return { - status: 200, - body: environments, - } - }, + // return { + // status: 200, + // body: environments, + // } + // }, - createEnvironment: async ({ request: req, body: requestBody }) => { - const projectId = requestBody.projectId - const perms = await authUser(req, { id: projectId }) + // createEnvironment: async ({ request: req, body: requestBody }) => { + // const projectId = requestBody.projectId + // const perms = await authUser(req, { id: projectId }) - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + // if (!perms.projectPermissions) return new NotFound404() + // if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - const checkCreateResult = await checkEnvironmentCreate({ ...requestBody }) - if (checkCreateResult.isError) return new BadRequest400(checkCreateResult.error) + // const checkCreateResult = await checkEnvironmentCreate({ ...requestBody }) + // if (checkCreateResult.isError) return new BadRequest400(checkCreateResult.error) - const result = await createEnvironment({ - userId: perms.user.id, - projectId, - name: requestBody.name, - clusterId: requestBody.clusterId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - stageId: requestBody.stageId, - requestId: req.id, - }) - if (result.isError) { - return new Internal500(result.error) - } - return { - status: 201, - body: result.data, - } - }, + // const result = await createEnvironment({ + // userId: perms.user.id, + // projectId, + // name: requestBody.name, + // clusterId: requestBody.clusterId, + // cpu: requestBody.cpu, + // gpu: requestBody.gpu, + // memory: requestBody.memory, + // stageId: requestBody.stageId, + // requestId: req.id, + // }) + // if (result.isError) { + // return new Internal500(result.error) + // } + // return { + // status: 201, + // body: result.data, + // } + // }, - updateEnvironment: async ({ request: req, body: requestBody, params }) => { - const { environmentId } = params - const perms = await authUser(req, { environmentId }) - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!ProjectAuthorized.ListEnvironments(perms)) return new NotFound404() - if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + // updateEnvironment: async ({ request: req, body: requestBody, params }) => { + // const { environmentId } = params + // const perms = await authUser(req, { environmentId }) + // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + // if (!ProjectAuthorized.ListEnvironments(perms)) return new NotFound404() + // if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - const checkUpdateResult = await checkEnvironmentUpdate({ environmentId, ...requestBody }) - if (checkUpdateResult.isError) return new BadRequest400(checkUpdateResult.error) + // const checkUpdateResult = await checkEnvironmentUpdate({ environmentId, ...requestBody }) + // if (checkUpdateResult.isError) return new BadRequest400(checkUpdateResult.error) - const result = await updateEnvironment({ - user: perms.user, - environmentId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - requestId: req.id, - }) - if (result.isError) { - return new Internal500(result.error) - } - return { - status: 200, - body: result.data, - } - }, + // const result = await updateEnvironment({ + // user: perms.user, + // environmentId, + // cpu: requestBody.cpu, + // gpu: requestBody.gpu, + // memory: requestBody.memory, + // requestId: req.id, + // }) + // if (result.isError) { + // return new Internal500(result.error) + // } + // return { + // status: 200, + // body: result.data, + // } + // }, - deleteEnvironment: async ({ request: req, params }) => { - const { environmentId } = params - const perms = await authUser(req, { environmentId }) - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + // deleteEnvironment: async ({ request: req, params }) => { + // const { environmentId } = params + // const perms = await authUser(req, { environmentId }) + // if (!perms.projectPermissions) return new NotFound404() + // if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - const result = await deleteEnvironment({ - userId: perms.user?.id, - environmentId, - requestId: req.id, - projectId: perms.projectId, - }) - if (result.isError) { - return new Internal500(result.error) - } + // const result = await deleteEnvironment({ + // userId: perms.user?.id, + // environmentId, + // requestId: req.id, + // projectId: perms.projectId, + // }) + // if (result.isError) { + // return new Internal500(result.error) + // } - return { - status: 204, - body: result.data, - } - }, - }) -} + // return { + // status: 204, + // body: result.data, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts index 7082924b6..29f166eda 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts @@ -1,49 +1,49 @@ -import type { FastifyInstance } from 'fastify' -import { serverInstance } from '@old-server/app' +// import type { FastifyInstance } from 'fastify' +// import { serverInstance } from '@old-server/app' -import { adminRoleRouter } from './admin-role/router' -import { adminTokenRouter } from './admin-token/router' -import { clusterRouter } from './cluster/router' -import { environmentRouter } from './environment/router' -import { logRouter } from './log/router' -import { personalAccessTokenRouter } from './user/tokens/router' -import { pluginConfigRouter } from './system/config/router' -import { projectMemberRouter } from './project-member/router' -import { projectRoleRouter } from './project-role/router' -import { projectRouter } from './project/router' -import { projectServiceRouter } from './project-service/router' -import { repositoryRouter } from './repository/router' -import { serviceChainRouter } from './service-chain/router' -import { serviceMonitorRouter } from './service-monitor/router' -import { stageRouter } from './stage/router' -import { systemRouter } from './system/router' -import { systemSettingsRouter } from './system/settings/router' -import { userRouter } from './user/router' -import { zoneRouter } from './zone/router' +// import { adminRoleRouter } from './admin-role/router' +// import { adminTokenRouter } from './admin-token/router' +// import { clusterRouter } from './cluster/router' +// import { environmentRouter } from './environment/router' +// import { logRouter } from './log/router' +// import { personalAccessTokenRouter } from './user/tokens/router' +// import { pluginConfigRouter } from './system/config/router' +// import { projectMemberRouter } from './project-member/router' +// import { projectRoleRouter } from './project-role/router' +// import { projectRouter } from './project/router' +// import { projectServiceRouter } from './project-service/router' +// import { repositoryRouter } from './repository/router' +// import { serviceChainRouter } from './service-chain/router' +// import { serviceMonitorRouter } from './service-monitor/router' +// import { stageRouter } from './stage/router' +// import { systemRouter } from './system/router' +// import { systemSettingsRouter } from './system/settings/router' +// import { userRouter } from './user/router' +// import { zoneRouter } from './zone/router' -// relax validation schema if NO_VALIDATION env var is set to true. -// /!\ It can lead to security leaks !!!! -const validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' } -export function apiRouter() { - return async (app: FastifyInstance) => { - await app.register(serverInstance.plugin(adminRoleRouter()), validateTrue) - await app.register(serverInstance.plugin(adminTokenRouter()), validateTrue) - await app.register(serverInstance.plugin(clusterRouter()), validateTrue) - await app.register(serverInstance.plugin(serviceChainRouter()), validateTrue) - await app.register(serverInstance.plugin(environmentRouter()), validateTrue) - await app.register(serverInstance.plugin(logRouter()), validateTrue) - await app.register(serverInstance.plugin(personalAccessTokenRouter()), validateTrue) - await app.register(serverInstance.plugin(projectRouter()), validateTrue) - await app.register(serverInstance.plugin(projectMemberRouter()), validateTrue) - await app.register(serverInstance.plugin(projectRoleRouter()), validateTrue) - await app.register(serverInstance.plugin(projectServiceRouter()), validateTrue) - await app.register(serverInstance.plugin(repositoryRouter()), validateTrue) - await app.register(serverInstance.plugin(serviceMonitorRouter()), validateTrue) - await app.register(serverInstance.plugin(pluginConfigRouter()), validateTrue) - await app.register(serverInstance.plugin(stageRouter()), validateTrue) - await app.register(serverInstance.plugin(systemRouter()), validateTrue) - await app.register(serverInstance.plugin(systemSettingsRouter()), validateTrue) - await app.register(serverInstance.plugin(userRouter()), validateTrue) - await app.register(serverInstance.plugin(zoneRouter()), validateTrue) - } -} +// // relax validation schema if NO_VALIDATION env var is set to true. +// // /!\ It can lead to security leaks !!!! +// const validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' } +// export function apiRouter() { + // return async (app: FastifyInstance) => { + // await app.register(serverInstance.plugin(adminRoleRouter()), validateTrue) + // await app.register(serverInstance.plugin(adminTokenRouter()), validateTrue) + // await app.register(serverInstance.plugin(clusterRouter()), validateTrue) + // await app.register(serverInstance.plugin(serviceChainRouter()), validateTrue) + // await app.register(serverInstance.plugin(environmentRouter()), validateTrue) + // await app.register(serverInstance.plugin(logRouter()), validateTrue) + // await app.register(serverInstance.plugin(personalAccessTokenRouter()), validateTrue) + // await app.register(serverInstance.plugin(projectRouter()), validateTrue) + // await app.register(serverInstance.plugin(projectMemberRouter()), validateTrue) + // await app.register(serverInstance.plugin(projectRoleRouter()), validateTrue) + // await app.register(serverInstance.plugin(projectServiceRouter()), validateTrue) + // await app.register(serverInstance.plugin(repositoryRouter()), validateTrue) + // await app.register(serverInstance.plugin(serviceMonitorRouter()), validateTrue) + // await app.register(serverInstance.plugin(pluginConfigRouter()), validateTrue) + // await app.register(serverInstance.plugin(stageRouter()), validateTrue) + // await app.register(serverInstance.plugin(systemRouter()), validateTrue) + // await app.register(serverInstance.plugin(systemSettingsRouter()), validateTrue) + // await app.register(serverInstance.plugin(userRouter()), validateTrue) + // await app.register(serverInstance.plugin(zoneRouter()), validateTrue) + // } +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts index 9449b012d..7894fdcfc 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts @@ -1,32 +1,32 @@ -import type { CleanLog, Log, XOR } from '@cpn-console/shared' -import { AdminAuthorized, logContract } from '@cpn-console/shared' -import { getLogs } from './business' -import { serverInstance } from '@old-server/app' -import type { UserProfile, UserProjectProfile } from '@old-server/utils/controller' -import { authUser } from '@old-server/utils/controller' -import { Forbidden403 } from '@old-server/utils/errors' +// import type { CleanLog, Log, XOR } from '@cpn-console/shared' +// import { AdminAuthorized, logContract } from '@cpn-console/shared' +// import { getLogs } from './business' +// import { serverInstance } from '@old-server/app' +// import type { UserProfile, UserProjectProfile } from '@old-server/utils/controller' +// import { authUser } from '@old-server/utils/controller' +// import { Forbidden403 } from '@old-server/utils/errors' -export function logRouter() { - return serverInstance.router(logContract, { - // Récupérer des logs - getLogs: async ({ request: req, query }) => { - const perms: XOR = query.projectId - ? await authUser(req, { id: query.projectId }) - : await authUser(req) +// export function logRouter() { + // return serverInstance.router(logContract, { + // // Récupérer des logs + // getLogs: async ({ request: req, query }) => { + // const perms: XOR = query.projectId + // ? await authUser(req, { id: query.projectId }) + // : await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { - if (!perms.projectPermissions) { - return new Forbidden403() - } - query.clean = true - } + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { + // if (!perms.projectPermissions) { + // return new Forbidden403() + // } + // query.clean = true + // } - const [total, logs] = await getLogs(query) as [number, unknown[]] as [number, Array] + // const [total, logs] = await getLogs(query) as [number, unknown[]] as [number, Array] - return { - status: 200, - body: { total, logs }, - } - }, - }) -} + // return { + // status: 200, + // body: { total, logs }, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts index 6ba44b71f..900debd22 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts @@ -1,82 +1,82 @@ -import { AdminAuthorized, ProjectAuthorized, projectMemberContract } from '@cpn-console/shared' -import { - addMember, - listMembers, - patchMembers, - removeMember, -} from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' - -export function projectMemberRouter() { - return serverInstance.router(projectMemberContract, { - listMembers: async ({ request: req, params }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - const body = await listMembers(projectId) - - return { - status: 200, - body, - } - }, - - addMember: async ({ request: req, params, body }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await addMember(projectId, body, perms.user.id, req.id, perms.projectOwnerId) - if (resBody instanceof ErrorResType) return resBody - - return { - status: 201, - body: resBody, - } - }, - - patchMembers: async ({ request: req, params, body }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await patchMembers(projectId, body) - - return { - status: 200, - body: resBody, - } - }, - - removeMember: async ({ request: req, params }) => { - const { projectId, userId } = params - const perms = await authUser(req, { id: projectId }) - - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - if (!ProjectAuthorized.ManageMembers(perms) && userId !== perms.user?.id) return new Forbidden403() - - const resBody = await removeMember(projectId, params.userId) - - return { - status: 200, - body: resBody, - } - }, - }) -} +// import { AdminAuthorized, ProjectAuthorized, projectMemberContract } from '@cpn-console/shared' +// import { + // addMember, + // listMembers, + // patchMembers, + // removeMember, +// } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' + +// export function projectMemberRouter() { + // return serverInstance.router(projectMemberContract, { + // listMembers: async ({ request: req, params }) => { + // const { projectId } = params + // const perms = await authUser(req, { id: projectId }) + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + // const body = await listMembers(projectId) + + // return { + // status: 200, + // body, + // } + // }, + + // addMember: async ({ request: req, params, body }) => { + // const { projectId } = params + // const perms = await authUser(req, { id: projectId }) + + // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + // if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const resBody = await addMember(projectId, body, perms.user.id, req.id, perms.projectOwnerId) + // if (resBody instanceof ErrorResType) return resBody + + // return { + // status: 201, + // body: resBody, + // } + // }, + + // patchMembers: async ({ request: req, params, body }) => { + // const { projectId } = params + // const perms = await authUser(req, { id: projectId }) + + // if (!perms.projectPermissions) return new NotFound404() + // if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const resBody = await patchMembers(projectId, body) + + // return { + // status: 200, + // body: resBody, + // } + // }, + + // removeMember: async ({ request: req, params }) => { + // const { projectId, userId } = params + // const perms = await authUser(req, { id: projectId }) + + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + // if (!ProjectAuthorized.ManageMembers(perms) && userId !== perms.user?.id) return new Forbidden403() + + // const resBody = await removeMember(projectId, params.userId) + + // return { + // status: 200, + // body: resBody, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts index 44a7e4e1a..2773fabe9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts @@ -1,90 +1,90 @@ -import { AdminAuthorized, ProjectAuthorized, projectRoleContract } from '@cpn-console/shared' -import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403, NotFound404 } from '@old-server/utils/errors' - -export function projectRoleRouter() { - return serverInstance.router(projectRoleContract, { - // Récupérer des projets - listProjectRoles: async ({ request: req, params }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - const body = await listRoles(projectId) - - return { - status: 200, - body, - } - }, - - createProjectRole: async ({ request: req, params: { projectId }, body }) => { - const perms = await authUser(req, { id: projectId }) - - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await createRole(projectId, body) - - return { - status: 201, - body: resBody, - } - }, - - patchProjectRoles: async ({ request: req, params: { projectId }, body }) => { - const perms = await authUser(req, { id: projectId }) - - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await patchRoles(projectId, body) - if (resBody instanceof ErrorResType) return resBody - - return { - status: 200, - body: resBody, - } - }, - - projectRoleMemberCounts: async ({ request: req, params }) => { - const { projectId } = params - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - const resBody = await countRolesMembers(projectId) - - return { - status: 200, - body: resBody, - } - }, - - deleteProjectRole: async ({ request: req, params: { projectId, roleId } }) => { - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const resBody = await deleteRole(roleId) - - return { - status: 204, - body: resBody, - } - }, - }) -} +// import { AdminAuthorized, ProjectAuthorized, projectRoleContract } from '@cpn-console/shared' +// import { + // countRolesMembers, + // createRole, + // deleteRole, + // listRoles, + // patchRoles, +// } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403, NotFound404 } from '@old-server/utils/errors' + +// export function projectRoleRouter() { + // return serverInstance.router(projectRoleContract, { + // // Récupérer des projets + // listProjectRoles: async ({ request: req, params }) => { + // const { projectId } = params + // const perms = await authUser(req, { id: projectId }) + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + // const body = await listRoles(projectId) + + // return { + // status: 200, + // body, + // } + // }, + + // createProjectRole: async ({ request: req, params: { projectId }, body }) => { + // const perms = await authUser(req, { id: projectId }) + + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + // if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const resBody = await createRole(projectId, body) + + // return { + // status: 201, + // body: resBody, + // } + // }, + + // patchProjectRoles: async ({ request: req, params: { projectId }, body }) => { + // const perms = await authUser(req, { id: projectId }) + + // if (!perms.projectPermissions) return new NotFound404() + // if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const resBody = await patchRoles(projectId, body) + // if (resBody instanceof ErrorResType) return resBody + + // return { + // status: 200, + // body: resBody, + // } + // }, + + // projectRoleMemberCounts: async ({ request: req, params }) => { + // const { projectId } = params + // const perms = await authUser(req, { id: projectId }) + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + + // const resBody = await countRolesMembers(projectId) + + // return { + // status: 200, + // body: resBody, + // } + // }, + + // deleteProjectRole: async ({ request: req, params: { projectId, roleId } }) => { + // const perms = await authUser(req, { id: projectId }) + // if (!perms.projectPermissions) return new NotFound404() + // if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const resBody = await deleteRole(roleId) + + // return { + // status: 204, + // body: resBody, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts index a02f6cbe3..2fa4a7eb2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts @@ -1,38 +1,38 @@ -import { AdminAuthorized, ProjectAuthorized, projectServiceContract } from '@cpn-console/shared' -import { getProjectServices, updateProjectServices } from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { Forbidden403, NotFound404 } from '@old-server/utils/errors' +// import { AdminAuthorized, ProjectAuthorized, projectServiceContract } from '@cpn-console/shared' +// import { getProjectServices, updateProjectServices } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { Forbidden403, NotFound404 } from '@old-server/utils/errors' -export function projectServiceRouter() { - return serverInstance.router(projectServiceContract, { - // Récupérer les services d'un projet - getServices: async ({ request: req, params: { projectId }, query }) => { - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!AdminAuthorized.isAdmin(perms.adminPermissions) && query.permissionTarget === 'admin') return new Forbidden403('Vous ne pouvez pas demander les paramètres admin') +// export function projectServiceRouter() { + // return serverInstance.router(projectServiceContract, { + // // Récupérer les services d'un projet + // getServices: async ({ request: req, params: { projectId }, query }) => { + // const perms = await authUser(req, { id: projectId }) + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + // if (!AdminAuthorized.isAdmin(perms.adminPermissions) && query.permissionTarget === 'admin') return new Forbidden403('Vous ne pouvez pas demander les paramètres admin') - const body = await getProjectServices(projectId, query.permissionTarget) + // const body = await getProjectServices(projectId, query.permissionTarget) - return { - status: 200, - body, - } - }, + // return { + // status: 200, + // body, + // } + // }, - updateProjectServices: async ({ request: req, params: { projectId }, body }) => { - const perms = await authUser(req, { id: projectId }) - if (!ProjectAuthorized.Manage(perms)) return new NotFound404() - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // updateProjectServices: async ({ request: req, params: { projectId }, body }) => { + // const perms = await authUser(req, { id: projectId }) + // if (!ProjectAuthorized.Manage(perms)) return new NotFound404() + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - const allowedRoles: Array<'user' | 'admin'> = AdminAuthorized.isAdmin(perms.adminPermissions) ? ['user', 'admin'] : ['user'] + // const allowedRoles: Array<'user' | 'admin'> = AdminAuthorized.isAdmin(perms.adminPermissions) ? ['user', 'admin'] : ['user'] - const resBody = await updateProjectServices(projectId, body, allowedRoles) - return { - status: 204, - body: resBody, - } - }, - }) -} + // const resBody = await updateProjectServices(projectId, body, allowedRoles) + // return { + // status: 204, + // body: resBody, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts index 2d1a92a67..915000520 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts @@ -1,261 +1,409 @@ -import { json2csv } from 'json-2-csv' -import { servicesInfos } from '@cpn-console/hooks' -import type { Project, User } from '@prisma/client' -import type { projectContract } from '@cpn-console/shared' -import { ProjectStatusSchema } from '@cpn-console/shared' +import { servicesInfos } from '@cpn-console/hooks'; +import type { projectContract } from '@cpn-console/shared'; +import { ProjectStatusSchema } from '@cpn-console/shared'; +import prisma from '@old-server/prisma'; import { - addLogs, - deleteAllEnvironmentForProject, - deleteAllRepositoryForProject, - getAllProjectsDataForExport, - getProjectOrThrow, - getSlugs, - initializeProject, - listProjects as listProjectsQuery, - lockProject, - updateProject as updateProjectQuery, -} from '@old-server/resources/queries-index' -import type { ErrorResType } from '@old-server/utils/errors' -import { BadRequest400, Forbidden403, Unprocessable422 } from '@old-server/utils/errors' -import { whereBuilder } from '@old-server/utils/controller' -import { hook } from '@old-server/utils/hook-wrapper' -import type { UserDetails } from '@old-server/types/index' -import prisma from '@old-server/prisma' -import { parallelBulkLimit } from '@old-server/utils/env' + addLogs, + deleteAllEnvironmentForProject, + deleteAllRepositoryForProject, + getAllProjectsDataForExport, + getProjectOrThrow, + getSlugs, + initializeProject, + listProjects as listProjectsQuery, + lockProject, + updateProject as updateProjectQuery, +} from '@old-server/resources/queries-index'; +import type { UserDetails } from '@old-server/types/index'; +import { whereBuilder } from '@old-server/utils/controller'; +import type { ErrorResType } from '@old-server/utils/errors'; +import { + BadRequest400, + Forbidden403, + Unprocessable422, +} from '@old-server/utils/errors'; +import { hook } from '@old-server/utils/hook-wrapper'; +import type { Project, User } from '@prisma/client'; +import { json2csv } from 'json-2-csv'; + +// server tuning +const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT + ? Number(process.env.PARALLEL_BULK_LIMIT) + : 5; export function generateSlug(prefix: string, existingSlugs?: string[]) { - if (!existingSlugs?.includes(prefix)) { - return prefix - } - let idx = 1 - let generated = `${prefix}-${idx}` - while (existingSlugs.includes(generated)) { - idx++ - generated = `${prefix}-${idx}` - } - return generated + if (!existingSlugs?.includes(prefix)) { + return prefix; + } + let idx = 1; + let generated = `${prefix}-${idx}`; + while (existingSlugs.includes(generated)) { + idx++; + generated = `${prefix}-${idx}`; + } + return generated; } -const projectStatus = ProjectStatusSchema._def.values -export async function listProjects({ status, statusIn, statusNotIn, filter = 'member', ...query }: typeof projectContract.listProjects.query._type, userId: User['id'] | undefined) { - return listProjectsQuery({ - ...query, - status: whereBuilder({ enumValues: projectStatus, eqValue: status, inValues: statusIn, notInValues: statusNotIn }), - filter, - userId, - }).then(projects => projects.map(({ clusters, ...project }) => ({ - ...project, - clusterIds: clusters.map(({ id }) => id), - roles: project.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), - everyonePerms: project.everyonePerms.toString(), - }))) +const projectStatus = ProjectStatusSchema._def.values; +export async function listProjects( + { + status, + statusIn, + statusNotIn, + filter = 'member', + ...query + }: typeof projectContract.listProjects.query._type, + userId: User['id'] | undefined, +) { + return listProjectsQuery({ + ...query, + status: whereBuilder({ + enumValues: projectStatus, + eqValue: status, + inValues: statusIn, + notInValues: statusNotIn, + }), + filter, + userId, + }).then((projects) => + projects.map(({ clusters, ...project }) => ({ + ...project, + clusterIds: clusters.map(({ id }) => id), + roles: project.roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + everyonePerms: project.everyonePerms.toString(), + })), + ); } export async function getProjectSecrets(projectId: string) { - const hookReply = await hook.project.getSecrets(projectId) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la récupération des secrets du projet') - } + const hookReply = await hook.project.getSecrets(projectId); + if (hookReply.failed) { + return new Unprocessable422( + 'Echec des services à la récupération des secrets du projet', + ); + } - return Object.fromEntries( - Object.entries(hookReply.results) - // @ts-ignore - .filter(([_key, value]) => Object.keys(value.secrets).length) - // @ts-ignore - .map(([key, value]) => [servicesInfos[key]?.title, value.secrets]), - ) + return Object.fromEntries( + Object.entries(hookReply.results) + // @ts-ignore + .filter(([_key, value]) => Object.keys(value.secrets).length) + // @ts-ignore + .map(([key, value]) => [servicesInfos[key]?.title, value.secrets]), + ); } -export async function createProject(dataDto: typeof projectContract.createProject.body._type, requestor: UserDetails, requestId: string) { - if (requestor.type !== 'human') return new BadRequest400('Seuls les comptes humains peuvent créer des projets') +export async function createProject( + dataDto: typeof projectContract.createProject.body._type, + requestor: UserDetails, + requestId: string, +) { + if (requestor.type !== 'human') + return new BadRequest400( + 'Seuls les comptes humains peuvent créer des projets', + ); - let slug = dataDto.name - const projectsWithSamePrefix = await getSlugs(slug) - slug = generateSlug(slug, projectsWithSamePrefix?.map(project => project.slug)) + let slug = dataDto.name; + const projectsWithSamePrefix = await getSlugs(slug); + slug = generateSlug( + slug, + projectsWithSamePrefix?.map((project) => project.slug), + ); - // Actions - const project = await initializeProject({ ...dataDto, slug, ownerId: requestor.id }) + // Actions + const project = await initializeProject({ + ...dataDto, + slug, + ownerId: requestor.id, + }); - const { results, project: projectInfos } = await hook.project.upsert(project.id) - await addLogs({ action: 'Create Project', data: results, userId: requestor.id, requestId, projectId: project.id }) - if (results.failed) { - return new Unprocessable422('Echec des services à la création du projet') - } + const { results, project: projectInfos } = await hook.project.upsert( + project.id, + ); + await addLogs({ + action: 'Create Project', + data: results, + userId: requestor.id, + requestId, + projectId: project.id, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services à la création du projet', + ); + } - return { - ...projectInfos, - clusterIds: projectInfos.clusters.map(({ id }) => id), - everyonePerms: projectInfos.everyonePerms.toString(), - roles: projectInfos.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), - } + return { + ...projectInfos, + clusterIds: projectInfos.clusters.map(({ id }) => id), + everyonePerms: projectInfos.everyonePerms.toString(), + roles: projectInfos.roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + }; } export async function getProject(projectId: Project['id']) { - return getProjectOrThrow(projectId).then(({ clusters, ...project }) => ({ - ...project, - clusterIds: clusters.map(({ id }) => id), - roles: project.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), - everyonePerms: project.everyonePerms.toString(), - })) + return getProjectOrThrow(projectId).then(({ clusters, ...project }) => ({ + ...project, + clusterIds: clusters.map(({ id }) => id), + roles: project.roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + everyonePerms: project.everyonePerms.toString(), + })); } export async function updateProject( - { description, ownerId: ownerIdCandidate, everyonePerms, locked, ...data }: typeof projectContract.updateProject.body._type, - projectId: Project['id'], - requestor: UserDetails, - requestId: string, + { + description, + ownerId: ownerIdCandidate, + everyonePerms, + locked, + ...data + }: typeof projectContract.updateProject.body._type, + projectId: Project['id'], + requestor: UserDetails, + requestId: string, ) { - // Actions - const projectDb = await prisma.project.findUniqueOrThrow({ - where: { id: projectId }, - include: { members: { include: { user: true } } }, - }) + // Actions + const projectDb = await prisma.project.findUniqueOrThrow({ + where: { id: projectId }, + include: { members: { include: { user: true } } }, + }); - if (projectDb.status === 'archived') return new Forbidden403('Le projet est archivé') + if (projectDb.status === 'archived') + return new Forbidden403('Le projet est archivé'); - if (ownerIdCandidate && ownerIdCandidate !== projectDb.ownerId) { - const memberCandidate = projectDb.members.find(member => member.userId === ownerIdCandidate) - if (!memberCandidate) { - return new BadRequest400('Le nouveau propriétaire doit faire partie des membres actuels du projet') - } - if (memberCandidate.user.type !== 'human') return new BadRequest400('Seuls les comptes humains peuvent être propriétaire de projets') - if (!projectDb.members.find(member => member.userId === projectDb.ownerId)) { - await prisma.projectMembers.create({ - data: { userId: projectDb.ownerId, projectId }, - }) + if (ownerIdCandidate && ownerIdCandidate !== projectDb.ownerId) { + const memberCandidate = projectDb.members.find( + (member) => member.userId === ownerIdCandidate, + ); + if (!memberCandidate) { + return new BadRequest400( + 'Le nouveau propriétaire doit faire partie des membres actuels du projet', + ); + } + if (memberCandidate.user.type !== 'human') + return new BadRequest400( + 'Seuls les comptes humains peuvent être propriétaire de projets', + ); + if ( + !projectDb.members.find( + (member) => member.userId === projectDb.ownerId, + ) + ) { + await prisma.projectMembers.create({ + data: { userId: projectDb.ownerId, projectId }, + }); + } + await prisma.$transaction([ + prisma.projectMembers.delete({ + where: { + projectId_userId: { userId: ownerIdCandidate, projectId }, + }, + }), + prisma.project.update({ + where: { id: projectId }, + data: { ownerId: ownerIdCandidate }, + }), + ]); } - await prisma.$transaction([ - prisma.projectMembers.delete({ - where: { projectId_userId: { userId: ownerIdCandidate, projectId } }, - }), - prisma.project.update({ where: { id: projectId }, data: { ownerId: ownerIdCandidate } }), - ]) - } - if (typeof description !== 'undefined' || typeof everyonePerms !== 'undefined' || typeof locked !== 'undefined') { - await updateProjectQuery(projectId, { - description, - locked, - ...everyonePerms && { everyonePerms: BigInt(everyonePerms) }, - ...data, - }) - } + if ( + typeof description !== 'undefined' || + typeof everyonePerms !== 'undefined' || + typeof locked !== 'undefined' + ) { + await updateProjectQuery(projectId, { + description, + locked, + ...(everyonePerms && { everyonePerms: BigInt(everyonePerms) }), + ...data, + }); + } - const { results, project: projectInfos } = await hook.project.upsert(projectId) - await addLogs({ action: 'Update Project', data: results, userId: requestor.id, requestId, projectId: projectInfos.id }) - if (results.failed) { - return new Unprocessable422('Echec des services à la mise à jour du projet') - } + const { results, project: projectInfos } = + await hook.project.upsert(projectId); + await addLogs({ + action: 'Update Project', + data: results, + userId: requestor.id, + requestId, + projectId: projectInfos.id, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services à la mise à jour du projet', + ); + } - return { - ...projectInfos, - clusterIds: projectInfos.clusters.map(({ id }) => id), - everyonePerms: projectInfos.everyonePerms.toString(), - roles: projectInfos.roles.map(role => ({ ...role, permissions: role.permissions.toString() })), - } + return { + ...projectInfos, + clusterIds: projectInfos.clusters.map(({ id }) => id), + everyonePerms: projectInfos.everyonePerms.toString(), + roles: projectInfos.roles.map((role) => ({ + ...role, + permissions: role.permissions.toString(), + })), + }; } interface ReplayHooksArgs { - projectId: Project['id'] - userId?: User['id'] - requestId: string + projectId: Project['id']; + userId?: User['id']; + requestId: string; } -export async function replayHooks({ projectId, userId, requestId }: ReplayHooksArgs): Promise { - const projectDb = await prisma.project.findUniqueOrThrow({ - where: { id: projectId }, - include: { members: { include: { user: true } } }, - }) - if (projectDb.locked) return new Forbidden403('Le projet est verrouillé') - if (projectDb.status === 'archived') return new Forbidden403('Le projet est archivé') - // Actions - const { results } = await hook.project.upsert(projectId) - await addLogs({ action: 'Replay hooks for Project', data: results, userId, requestId, projectId }) - if (results.failed) { - return new Unprocessable422('Echec des services au reprovisionnement du projet') - } - return null +export async function replayHooks({ + projectId, + userId, + requestId, +}: ReplayHooksArgs): Promise { + const projectDb = await prisma.project.findUniqueOrThrow({ + where: { id: projectId }, + include: { members: { include: { user: true } } }, + }); + if (projectDb.locked) return new Forbidden403('Le projet est verrouillé'); + if (projectDb.status === 'archived') + return new Forbidden403('Le projet est archivé'); + // Actions + const { results } = await hook.project.upsert(projectId); + await addLogs({ + action: 'Replay hooks for Project', + data: results, + userId, + requestId, + projectId, + }); + if (results.failed) { + return new Unprocessable422( + 'Echec des services au reprovisionnement du projet', + ); + } + return null; } -export async function archiveProject(projectId: Project['id'], requestor: UserDetails, requestId: string): Promise { - // Actions - // Empty the project first - const [projectDb, ..._] = await Promise.all([ - // get initial project state - prisma.project.findUniqueOrThrow({ where: { id: projectId } }), - deleteAllRepositoryForProject(projectId), - deleteAllEnvironmentForProject(projectId), - ]) +export async function archiveProject( + projectId: Project['id'], + requestor: UserDetails, + requestId: string, +): Promise { + // Actions + // Empty the project first + const [projectDb, ..._] = await Promise.all([ + // get initial project state + prisma.project.findUniqueOrThrow({ where: { id: projectId } }), + deleteAllRepositoryForProject(projectId), + deleteAllEnvironmentForProject(projectId), + ]); - if (projectDb.locked) return new Forbidden403('Le projet est verrouillé') - if (projectDb.status === 'archived') return new BadRequest400('Le projet est archivé') - if (projectDb.locked) { - await lockProject(projectId) - } + if (projectDb.locked) return new Forbidden403('Le projet est verrouillé'); + if (projectDb.status === 'archived') + return new BadRequest400('Le projet est archivé'); + if (projectDb.locked) { + await lockProject(projectId); + } - // -- début - Suppression projet -- - const { results, project } = await hook.project.delete(projectId) - await addLogs({ action: 'Delete all project resources', data: results, userId: requestor.id, requestId, projectId }) - if (project.status !== 'archived' && !projectDb.locked) { - await prisma.project.update({ where: { id: projectId }, data: { locked: false } }) - } - if (results.failed) { - return new Unprocessable422('Echec des services à la suppression du projet') - } + // -- début - Suppression projet -- + const { results, project } = await hook.project.delete(projectId); + await addLogs({ + action: 'Delete all project resources', + data: results, + userId: requestor.id, + requestId, + projectId, + }); + if (project.status !== 'archived' && !projectDb.locked) { + await prisma.project.update({ + where: { id: projectId }, + data: { locked: false }, + }); + } + if (results.failed) { + return new Unprocessable422( + 'Echec des services à la suppression du projet', + ); + } - // Retrait clusters -- - await prisma.project.update({ - where: { id: projectId }, - data: { - clusters: { set: [] }, - }, - }) + // Retrait clusters -- + await prisma.project.update({ + where: { id: projectId }, + data: { + clusters: { set: [] }, + }, + }); - // -- fin - Suppression projet -- - return null + // -- fin - Suppression projet -- + return null; } export async function generateProjectsData() { - const projects = await getAllProjectsDataForExport() + const projects = await getAllProjectsDataForExport(); - return json2csv(projects, { - emptyFieldValue: '', - }) + return json2csv(projects, { + emptyFieldValue: '', + }); } -export async function bulkActionProject(data: typeof projectContract.bulkActionProject.body._type, requestor: UserDetails, requestId: string) { - if (data.projectIds === 'all') { - data.projectIds = (await prisma.project.findMany({ - select: { id: true }, - where: { status: { not: 'archived' } }, - })).map(({ id }) => id) - } - bulkExector(data.projectIds - .map((projectId) => { - if (data.action === 'archive') { - return () => archiveProject(projectId, requestor, requestId) - } - if (data.action === 'lock') { - return () => updateProject({ locked: true }, projectId, requestor, requestId) - } - if (data.action === 'unlock') { - return () => updateProject({ locked: false }, projectId, requestor, requestId) - } - if (data.action === 'replay') { - return () => replayHooks({ projectId, userId: requestor.id, requestId }) - } - // should never been called - return async () => {} - })) +export async function bulkActionProject( + data: typeof projectContract.bulkActionProject.body._type, + requestor: UserDetails, + requestId: string, +) { + if (data.projectIds === 'all') { + data.projectIds = ( + await prisma.project.findMany({ + select: { id: true }, + where: { status: { not: 'archived' } }, + }) + ).map(({ id }) => id); + } + bulkExector( + data.projectIds.map((projectId) => { + if (data.action === 'archive') { + return () => archiveProject(projectId, requestor, requestId); + } + if (data.action === 'lock') { + return () => + updateProject( + { locked: true }, + projectId, + requestor, + requestId, + ); + } + if (data.action === 'unlock') { + return () => + updateProject( + { locked: false }, + projectId, + requestor, + requestId, + ); + } + if (data.action === 'replay') { + return () => + replayHooks({ projectId, userId: requestor.id, requestId }); + } + // should never been called + return async () => {}; + }), + ); } export function chunk(arr: T[], size: number): T[][] { - return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => - arr.slice(i * size, i * size + size)) + return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => + arr.slice(i * size, i * size + size), + ); } async function bulkExector(toExecute: Array<() => Promise>) { - const toExecuteChunked = chunk(toExecute, parallelBulkLimit) - for (const chunkToExecute of toExecuteChunked) { - await Promise.allSettled(chunkToExecute.map(fn => fn())) - } + const toExecuteChunked = chunk(toExecute, parallelBulkLimit); + for (const chunkToExecute of toExecuteChunked) { + await Promise.allSettled(chunkToExecute.map((fn) => fn())); + } } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts index 4cbf61b1a..dc4ff028b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts @@ -1,335 +1,371 @@ -import type { - Prisma, - Project, - User, -} from '@prisma/client' -import { - ProjectStatus, -} from '@prisma/client' -import type { XOR, projectContract } from '@cpn-console/shared' -import prisma from '@old-server/prisma' -import { appVersion } from '@old-server/utils/env' -import { uuid } from '@old-server/utils/queries-tools' +import type { XOR, projectContract } from '@cpn-console/shared'; +import prisma from '@old-server/prisma'; +import { uuid } from '@old-server/utils/queries-tools'; +import type { Prisma, Project, User } from '@prisma/client'; +import { ProjectStatus } from '@prisma/client'; -type ProjectUpdate = Partial> +// TODO: Convert to NestJS class and inject ConfigurationService to retrieve `appVersion` +const appVersion = + process.env.NODE_ENV === 'production' + ? (process.env.APP_VERSION ?? 'unknown') + : 'dev'; + +type ProjectUpdate = Partial< + Pick +>; export function updateProject(id: Project['id'], data: ProjectUpdate) { - return prisma.project.update({ - where: { id }, - data, - include: { members: true }, - }) + return prisma.project.update({ + where: { id }, + data, + include: { members: true }, + }); } // SELECT -type FilterWhere = XOR<{ - userId?: User['id'] - filter: 'all' -}, { - userId: User['id'] | undefined - filter: 'owned' | 'member' - }> -type ListProjectWhere = Omit<(typeof projectContract.listProjects.query._type), 'status_in' | 'status_not_in' | 'status'> & - Pick & - FilterWhere +type FilterWhere = XOR< + { + userId?: User['id']; + filter: 'all'; + }, + { + userId: User['id'] | undefined; + filter: 'owned' | 'member'; + } +>; +type ListProjectWhere = Omit< + typeof projectContract.listProjects.query._type, + 'status_in' | 'status_not_in' | 'status' +> & + Pick & + FilterWhere; export async function listProjects({ - description, - locked, - name, - status, - id, - filter, - userId, - search, - lastSuccessProvisionningVersion, + description, + locked, + name, + status, + id, + filter, + userId, + search, + lastSuccessProvisionningVersion, }: ListProjectWhere) { - const whereAnd: Prisma.ProjectWhereInput[] = [] - if (id) whereAnd.push({ id }) - if (locked != null) whereAnd.push({ locked }) - if (name) whereAnd.push({ name }) - if (status) whereAnd.push({ status }) - if (description) whereAnd.push({ description: { contains: description } }) - if (lastSuccessProvisionningVersion) { - if (lastSuccessProvisionningVersion === 'outdated') whereAnd.push({ lastSuccessProvisionningVersion: { not: appVersion } }) - else if (lastSuccessProvisionningVersion === 'last') whereAnd.push({ lastSuccessProvisionningVersion: { equals: appVersion } }) - else whereAnd.push({ lastSuccessProvisionningVersion }) - } - if (search) { - whereAnd.push({ OR: [{ - name: { contains: search }, - }, { - owner: { email: { contains: search } }, - }] }) - } + const whereAnd: Prisma.ProjectWhereInput[] = []; + if (id) whereAnd.push({ id }); + if (locked != null) whereAnd.push({ locked }); + if (name) whereAnd.push({ name }); + if (status) whereAnd.push({ status }); + if (description) whereAnd.push({ description: { contains: description } }); + if (lastSuccessProvisionningVersion) { + if (lastSuccessProvisionningVersion === 'outdated') + whereAnd.push({ + lastSuccessProvisionningVersion: { not: appVersion }, + }); + else if (lastSuccessProvisionningVersion === 'last') + whereAnd.push({ + lastSuccessProvisionningVersion: { equals: appVersion }, + }); + else whereAnd.push({ lastSuccessProvisionningVersion }); + } + if (search) { + whereAnd.push({ + OR: [ + { + name: { contains: search }, + }, + { + owner: { email: { contains: search } }, + }, + ], + }); + } - if (filter === 'owned') { - whereAnd.push({ ownerId: userId }) - } else if (filter === 'member') { - whereAnd.push({ OR: [{ - members: { some: { userId } }, - }, { - ownerId: userId, - }] }) - } + if (filter === 'owned') { + whereAnd.push({ ownerId: userId }); + } else if (filter === 'member') { + whereAnd.push({ + OR: [ + { + members: { some: { userId } }, + }, + { + ownerId: userId, + }, + ], + }); + } - return prisma.project.findMany({ - where: { AND: whereAnd }, - include: { - clusters: { select: { id: true } }, - members: { include: { user: true } }, - roles: true, - owner: true, - }, - }) + return prisma.project.findMany({ + where: { AND: whereAnd }, + include: { + clusters: { select: { id: true } }, + members: { include: { user: true } }, + roles: true, + owner: true, + }, + }); } export function getProjectOrThrow(id: Project['id'] | Project['slug']) { - return prisma.project.findFirstOrThrow({ - where: uuid.test(id) - ? { id } - : { slug: id }, - include: { - clusters: { select: { id: true } }, - members: { include: { user: true } }, - roles: true, - owner: true, - }, - }) + return prisma.project.findFirstOrThrow({ + where: uuid.test(id) ? { id } : { slug: id }, + include: { + clusters: { select: { id: true } }, + members: { include: { user: true } }, + roles: true, + owner: true, + }, + }); } export function getProjectInfosByIdOrThrow(projectId: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { - id: projectId, - }, - include: { - environments: true, - clusters: { include: { zone: true } }, - }, - }) + return prisma.project.findUniqueOrThrow({ + where: { + id: projectId, + }, + include: { + environments: true, + clusters: { include: { zone: true } }, + }, + }); } export function getProjectMembers(projectId: Project['id']) { - return prisma.projectMembers.findMany({ - where: { - projectId, - }, - include: { user: true }, - }) + return prisma.projectMembers.findMany({ + where: { + projectId, + }, + include: { user: true }, + }); } export function getProjectById(id: Project['id']) { - return prisma.project.findUnique({ where: { id } }) + return prisma.project.findUnique({ where: { id } }); } export const baseProjectIncludes = { - members: { include: { user: true } }, - clusters: true, - roles: true, - owner: true, -} as const + members: { include: { user: true } }, + clusters: true, + roles: true, + owner: true, +} as const; export function getProjectInfos(id: Project['id']) { - return prisma.project.findUnique({ - where: { id }, - include: baseProjectIncludes, - }) + return prisma.project.findUnique({ + where: { id }, + include: baseProjectIncludes, + }); } export function getProjectInfosOrThrow(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: baseProjectIncludes, - }) + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: baseProjectIncludes, + }); } export function getProjectInfosAndRepos(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: { - ...baseProjectIncludes, - repositories: true, - }, - }) + return prisma.project.findUniqueOrThrow({ + where: { id }, + include: { + ...baseProjectIncludes, + repositories: true, + }, + }); } export function getSlugs(slugPrefix: string) { - return prisma.project.findMany({ - where: { - slug: { startsWith: slugPrefix }, - }, - }) + return prisma.project.findMany({ + where: { + slug: { startsWith: slugPrefix }, + }, + }); } export function getAllProjectsDataForExport() { - return prisma.project.findMany({ - select: { - name: true, - description: true, - createdAt: true, - updatedAt: true, - environments: { + return prisma.project.findMany({ select: { - name: true, - stage: true, - cluster: { - select: { label: true }, - }, + name: true, + description: true, + createdAt: true, + updatedAt: true, + environments: { + select: { + name: true, + stage: true, + cluster: { + select: { label: true }, + }, + }, + }, + owner: true, }, - }, - owner: true, - }, - }) + }); } export function getRolesByProjectId(projectId: Project['id']) { - return prisma.projectRole.findMany({ - where: { projectId }, - }) + return prisma.projectRole.findMany({ + where: { projectId }, + }); } const clusterInfosSelect = { - id: true, - infos: true, - label: true, - external: true, - privacy: true, - secretName: true, - kubeconfig: true, - clusterResources: true, - cpu: true, - gpu: true, - memory: true, - zone: { - select: { - id: true, - slug: true, - argocdUrl: true, - label: true, + id: true, + infos: true, + label: true, + external: true, + privacy: true, + secretName: true, + kubeconfig: true, + clusterResources: true, + cpu: true, + gpu: true, + memory: true, + zone: { + select: { + id: true, + slug: true, + argocdUrl: true, + label: true, + }, }, - }, -} +}; export function getHookProjectInfos(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: { - members: { include: { user: true }, where: { user: { type: 'human' } } }, - clusters: { select: clusterInfosSelect }, - environments: { + return prisma.project.findUniqueOrThrow({ + where: { id }, include: { - stage: true, - cluster: { - select: clusterInfosSelect, - }, + members: { + include: { user: true }, + where: { user: { type: 'human' } }, + }, + clusters: { select: clusterInfosSelect }, + environments: { + include: { + stage: true, + cluster: { + select: clusterInfosSelect, + }, + }, + }, + repositories: true, + plugins: { + select: { + key: true, + pluginName: true, + value: true, + }, + }, + owner: true, + roles: true, }, - }, - repositories: true, - plugins: { - select: { - key: true, - pluginName: true, - value: true, - }, - }, - owner: true, - roles: true, - }, - }) + }); } // CREATE interface CreateProjectParams { - name: Project['name'] - description?: Project['description'] - ownerId: User['id'] - slug: Project['slug'] - limitless: boolean - hprodCpu: number - hprodGpu: number - hprodMemory: number - prodCpu: number - prodGpu: number - prodMemory: number + name: Project['name']; + description?: Project['description']; + ownerId: User['id']; + slug: Project['slug']; + limitless: boolean; + hprodCpu: number; + hprodGpu: number; + hprodMemory: number; + prodCpu: number; + prodGpu: number; + prodMemory: number; } export function initializeProject(params: CreateProjectParams) { - return prisma.project.create({ - data: { - description: params.description ?? '', - status: ProjectStatus.created, - locked: false, - ...params, - }, - }) + return prisma.project.create({ + data: { + description: params.description ?? '', + status: ProjectStatus.created, + locked: false, + ...params, + }, + }); } // UPDATE export function lockProject(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { locked: true }, - }) + return prisma.project.update({ + where: { id }, + data: { locked: true }, + }); } export function updateProjectCreated(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { - status: ProjectStatus.created, - lastSuccessProvisionningVersion: appVersion, - }, - include: baseProjectIncludes, - }) + return prisma.project.update({ + where: { id }, + data: { + status: ProjectStatus.created, + lastSuccessProvisionningVersion: appVersion, + }, + include: baseProjectIncludes, + }); } export function updateProjectFailed(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { status: ProjectStatus.failed }, - include: baseProjectIncludes, - }) + return prisma.project.update({ + where: { id }, + data: { status: ProjectStatus.failed }, + include: baseProjectIncludes, + }); } export function updateProjectWarning(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { status: ProjectStatus.warning }, - include: baseProjectIncludes, - }) + return prisma.project.update({ + where: { id }, + data: { status: ProjectStatus.warning }, + include: baseProjectIncludes, + }); } -export function addUserToProject({ project, user }: { project: Project, user: User }) { - return prisma.projectMembers.create({ - data: { - userId: user.id, - projectId: project.id, - }, - }) +export function addUserToProject({ + project, + user, +}: { + project: Project; + user: User; +}) { + return prisma.projectMembers.create({ + data: { + userId: user.id, + projectId: project.id, + }, + }); } -export function removeUserFromProject({ projectId, userId }: { projectId: Project['id'], userId: User['id'] }) { - return prisma.projectMembers.delete({ - where: { - projectId_userId: { - projectId, - userId, - }, - }, - }) +export function removeUserFromProject({ + projectId, + userId, +}: { + projectId: Project['id']; + userId: User['id']; +}) { + return prisma.projectMembers.delete({ + where: { + projectId_userId: { + projectId, + userId, + }, + }, + }); } export async function archiveProject(id: Project['id']) { - const project = await prisma.project.findUnique({ - where: { id }, - select: { name: true, slug: true }, - }) - return prisma.project.update({ - where: { id }, - data: { - name: `${project?.name}_${Date.now()}_archived`, - slug: `${project?.slug}_${Date.now()}_archived`, - status: ProjectStatus.archived, - locked: true, - }, - include: baseProjectIncludes, - }) + const project = await prisma.project.findUnique({ + where: { id }, + select: { name: true, slug: true }, + }); + return prisma.project.update({ + where: { id }, + data: { + name: `${project?.name}_${Date.now()}_archived`, + slug: `${project?.slug}_${Date.now()}_archived`, + status: ProjectStatus.archived, + locked: true, + }, + include: baseProjectIncludes, + }); } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts index 497b3f860..c3f737324 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts @@ -1,199 +1,199 @@ -import type { AsyncReturnType } from '@cpn-console/shared' -import { AdminAuthorized, ProjectAuthorized, projectContract } from '@cpn-console/shared' -import { - archiveProject, - bulkActionProject, - createProject, - generateProjectsData, - getProject, - getProjectSecrets, - listProjects, - replayHooks, - updateProject, -} from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' - -export function projectRouter() { - return serverInstance.router(projectContract, { - - // Récupérer des projets - listProjects: async ({ request: req, query }) => { - const { adminPermissions, user } = await authUser(req) - let body: AsyncReturnType = [] - - if (adminPermissions && !user) { // c'est donc un compte de service - query.filter = 'all' - } - if (query.filter === 'all' && !AdminAuthorized.isAdmin(adminPermissions)) { - return new BadRequest400('Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre \'all\'') - } - - body = await listProjects( - query, - user?.id, - ) - - return { - status: 200, - body, - } - }, - - // Récupérer les secrets d'un projet - getProjectSecrets: async ({ request: req, params }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.SeeSecrets(perms)) return new Forbidden403() - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const body = await getProjectSecrets(projectId) - - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - // Créer un projet - createProject: async ({ request: req, body: data }) => { - const perms = await authUser(req) - if (perms.user?.type !== 'human') return new Unauthorized401('Cannot find requestor in database') - const body = await createProject(data, perms.user, req.id) - - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - // Récuperer un seul projet - getProject: async ({ request: req, params }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - if (!perms.projectId) return new NotFound404() - if (!isAdmin) { - if (!perms.projectPermissions) { - return new NotFound404() - } - if (perms.projectStatus === 'archived') { - return new NotFound404() - } - } - - const body = await getProject(projectId) - - return { - status: 200, - body, - } - }, - - // Mettre à jour un projet - updateProject: async ({ request: req, params, body: data }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - - if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - const isOwner = perms.projectOwnerId === perms.user.id - - if (!perms.projectPermissions && !isAdmin) return new NotFound404() - if (!isAdmin) { // filtrage des clés par niveau de permissions - delete data.locked - if (!isOwner) { - delete data.ownerId // impossible de toucher à cette clé - } - } - if (perms.projectLocked) { - if (!isAdmin) return new Forbidden403('Le projet est verrouillé') - if (data.locked !== false) return new Forbidden403('Veuillez déverrouiler le projet pour le mettre à jour') - } - - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() - - const body = await updateProject(data, projectId, perms.user, req.id) - - if (body instanceof ErrorResType) return body - return { - status: 200, - body, - } - }, - - // Reprovisionner un projet - replayHooksForProject: async ({ request: req, params }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - if (!perms.projectPermissions && !isAdmin) return new NotFound404() - if (!ProjectAuthorized.ReplayHooks(perms)) return new Forbidden403() - - const body = await replayHooks({ - projectId, - userId: perms.user?.id, - requestId: req.id, - }) - - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - - // Archiver un projet - archiveProject: async ({ request: req, params }) => { - const projectId = params.projectId - const perms = await authUser(req, { id: projectId }) - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - if (!perms.projectPermissions && !isAdmin) return new NotFound404() - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() - - const body = await archiveProject(projectId, perms.user, req.id) - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - // Récupérer les données de tous les projets pour export - getProjectsData: async ({ request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const body = await generateProjectsData() - - return { - status: 200, - body, - } - }, - - bulkActionProject: async ({ request: req, body }) => { - const perms = await authUser(req) - - if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - await bulkActionProject(body, perms.user, req.id) - - return { - status: 202, - body: null, - } - }, - }) -} +// import type { AsyncReturnType } from '@cpn-console/shared' +// import { AdminAuthorized, ProjectAuthorized, projectContract } from '@cpn-console/shared' +// import { + // archiveProject, + // bulkActionProject, + // createProject, + // generateProjectsData, + // getProject, + // getProjectSecrets, + // listProjects, + // replayHooks, + // updateProject, +// } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' + +// export function projectRouter() { + // return serverInstance.router(projectContract, { + + // // Récupérer des projets + // listProjects: async ({ request: req, query }) => { + // const { adminPermissions, user } = await authUser(req) + // let body: AsyncReturnType = [] + + // if (adminPermissions && !user) { // c'est donc un compte de service + // query.filter = 'all' + // } + // if (query.filter === 'all' && !AdminAuthorized.isAdmin(adminPermissions)) { + // return new BadRequest400('Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre \'all\'') + // } + + // body = await listProjects( + // query, + // user?.id, + // ) + + // return { + // status: 200, + // body, + // } + // }, + + // // Récupérer les secrets d'un projet + // getProjectSecrets: async ({ request: req, params }) => { + // const projectId = params.projectId + // const perms = await authUser(req, { id: projectId }) + // if (!perms.projectPermissions) return new NotFound404() + // if (!ProjectAuthorized.SeeSecrets(perms)) return new Forbidden403() + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const body = await getProjectSecrets(projectId) + + // if (body instanceof ErrorResType) return body + + // return { + // status: 200, + // body, + // } + // }, + + // // Créer un projet + // createProject: async ({ request: req, body: data }) => { + // const perms = await authUser(req) + // if (perms.user?.type !== 'human') return new Unauthorized401('Cannot find requestor in database') + // const body = await createProject(data, perms.user, req.id) + + // if (body instanceof ErrorResType) return body + + // return { + // status: 201, + // body, + // } + // }, + + // // Récuperer un seul projet + // getProject: async ({ request: req, params }) => { + // const projectId = params.projectId + // const perms = await authUser(req, { id: projectId }) + // const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + // if (!perms.projectId) return new NotFound404() + // if (!isAdmin) { + // if (!perms.projectPermissions) { + // return new NotFound404() + // } + // if (perms.projectStatus === 'archived') { + // return new NotFound404() + // } + // } + + // const body = await getProject(projectId) + + // return { + // status: 200, + // body, + // } + // }, + + // // Mettre à jour un projet + // updateProject: async ({ request: req, params, body: data }) => { + // const projectId = params.projectId + // const perms = await authUser(req, { id: projectId }) + + // if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + // const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + // const isOwner = perms.projectOwnerId === perms.user.id + + // if (!perms.projectPermissions && !isAdmin) return new NotFound404() + // if (!isAdmin) { // filtrage des clés par niveau de permissions + // delete data.locked + // if (!isOwner) { + // delete data.ownerId // impossible de toucher à cette clé + // } + // } + // if (perms.projectLocked) { + // if (!isAdmin) return new Forbidden403('Le projet est verrouillé') + // if (data.locked !== false) return new Forbidden403('Veuillez déverrouiler le projet pour le mettre à jour') + // } + + // if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() + + // const body = await updateProject(data, projectId, perms.user, req.id) + + // if (body instanceof ErrorResType) return body + // return { + // status: 200, + // body, + // } + // }, + + // // Reprovisionner un projet + // replayHooksForProject: async ({ request: req, params }) => { + // const projectId = params.projectId + // const perms = await authUser(req, { id: projectId }) + // const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + // if (!perms.projectPermissions && !isAdmin) return new NotFound404() + // if (!ProjectAuthorized.ReplayHooks(perms)) return new Forbidden403() + + // const body = await replayHooks({ + // projectId, + // userId: perms.user?.id, + // requestId: req.id, + // }) + + // if (body instanceof ErrorResType) return body + + // return { + // status: 204, + // body, + // } + // }, + + // // Archiver un projet + // archiveProject: async ({ request: req, params }) => { + // const projectId = params.projectId + // const perms = await authUser(req, { id: projectId }) + // const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) + + // if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + // if (!perms.projectPermissions && !isAdmin) return new NotFound404() + // if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() + + // const body = await archiveProject(projectId, perms.user, req.id) + // if (body instanceof ErrorResType) return body + + // return { + // status: 204, + // body, + // } + // }, + // // Récupérer les données de tous les projets pour export + // getProjectsData: async ({ request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // const body = await generateProjectsData() + + // return { + // status: 200, + // body, + // } + // }, + + // bulkActionProject: async ({ request: req, body }) => { + // const perms = await authUser(req) + + // if (!perms.user) return new Unauthorized401('Cannot find requestor in database') + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // await bulkActionProject(body, perms.user, req.id) + + // return { + // status: 202, + // body: null, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts index 4ab014e1b..ddb740bd9 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts @@ -1,138 +1,138 @@ -import { AdminAuthorized, ProjectAuthorized, fakeToken, repositoryContract } from '@cpn-console/shared' -import { - createRepository, - deleteRepository, - getProjectRepositories, - syncRepository, - updateRepository, -} from './business' -import { serverInstance } from '@old-server/app' - -import { filterObjectByKeys } from '@old-server/utils/queries-tools' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' - -export function repositoryRouter() { - return serverInstance.router(repositoryContract, { - // Récupérer tous les repositories d'un projet - listRepositories: async ({ request: req, query }) => { - const projectId = query.projectId - const perms = await authUser(req, { id: projectId }) - - const body = ProjectAuthorized.ListRepositories(perms) - ? await getProjectRepositories(projectId) - : [] - - return { - status: 200, - body, - } - }, - - // Synchroniser un repository - syncRepository: async ({ request: req, params, body }) => { - const { repositoryId } = params - const perms = await authUser(req, { repositoryId }) - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const { syncAllBranches, branchName } = body - - const resBody = await syncRepository({ repositoryId, userId: perms.user.id, branchName, requestId: req.id, syncAllBranches }) - if (resBody instanceof ErrorResType) return resBody - - return { - status: 204, - body: resBody, - } - }, - - // Créer un repository - createRepository: async ({ request: req, body: data }) => { - const projectId = data.projectId - const perms = await authUser(req, { id: projectId }) - - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const body = await createRepository({ data, userId: perms.user.id, requestId: req.id }) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - // Mettre à jour un repository - updateRepository: async ({ request: req, params, body }) => { - const repositoryId = params.repositoryId - const perms = await authUser(req, { repositoryId }) - - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const keysAllowedForUpdate = [ - 'externalRepoUrl', - 'isPrivate', - 'externalToken', - 'externalUserName', - 'isInfra', - 'deployRevision', - 'deployPath', - 'helmValuesFiles', - ] - const data = filterObjectByKeys(body, keysAllowedForUpdate) - - if (data.externalToken === fakeToken) { - delete data.externalToken - } - - if (data.isPrivate === false) { - delete data.externalToken - delete data.externalUserName - } - - const resBody = await updateRepository({ repositoryId, data, userId: perms.user.id, requestId: req.id }) - if (resBody instanceof ErrorResType) return resBody - - return { - status: 200, - body: resBody, - } - }, - - // Supprimer un repository - deleteRepository: async ({ request: req, params }) => { - const repositoryId = params.repositoryId - const perms = await authUser(req, { repositoryId }) - - if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - if (!perms.projectPermissions) return new NotFound404() - if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - const body = await deleteRepository({ - repositoryId, - userId: perms.user.id, - requestId: req.id, - projectId: perms.projectId, - }) - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - }) -} +// import { AdminAuthorized, ProjectAuthorized, fakeToken, repositoryContract } from '@cpn-console/shared' +// import { + // createRepository, + // deleteRepository, + // getProjectRepositories, + // syncRepository, + // updateRepository, +// } from './business' +// import { serverInstance } from '@old-server/app' + +// import { filterObjectByKeys } from '@old-server/utils/queries-tools' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' + +// export function repositoryRouter() { + // return serverInstance.router(repositoryContract, { + // // Récupérer tous les repositories d'un projet + // listRepositories: async ({ request: req, query }) => { + // const projectId = query.projectId + // const perms = await authUser(req, { id: projectId }) + + // const body = ProjectAuthorized.ListRepositories(perms) + // ? await getProjectRepositories(projectId) + // : [] + + // return { + // status: 200, + // body, + // } + // }, + + // // Synchroniser un repository + // syncRepository: async ({ request: req, params, body }) => { + // const { repositoryId } = params + // const perms = await authUser(req, { repositoryId }) + // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + // if (!perms.projectPermissions) return new NotFound404() + // if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const { syncAllBranches, branchName } = body + + // const resBody = await syncRepository({ repositoryId, userId: perms.user.id, branchName, requestId: req.id, syncAllBranches }) + // if (resBody instanceof ErrorResType) return resBody + + // return { + // status: 204, + // body: resBody, + // } + // }, + + // // Créer un repository + // createRepository: async ({ request: req, body: data }) => { + // const projectId = data.projectId + // const perms = await authUser(req, { id: projectId }) + + // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + // if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const body = await createRepository({ data, userId: perms.user.id, requestId: req.id }) + // if (body instanceof ErrorResType) return body + + // return { + // status: 201, + // body, + // } + // }, + + // // Mettre à jour un repository + // updateRepository: async ({ request: req, params, body }) => { + // const repositoryId = params.repositoryId + // const perms = await authUser(req, { repositoryId }) + + // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() + // if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const keysAllowedForUpdate = [ + // 'externalRepoUrl', + // 'isPrivate', + // 'externalToken', + // 'externalUserName', + // 'isInfra', + // 'deployRevision', + // 'deployPath', + // 'helmValuesFiles', + // ] + // const data = filterObjectByKeys(body, keysAllowedForUpdate) + + // if (data.externalToken === fakeToken) { + // delete data.externalToken + // } + + // if (data.isPrivate === false) { + // delete data.externalToken + // delete data.externalUserName + // } + + // const resBody = await updateRepository({ repositoryId, data, userId: perms.user.id, requestId: req.id }) + // if (resBody instanceof ErrorResType) return resBody + + // return { + // status: 200, + // body: resBody, + // } + // }, + + // // Supprimer un repository + // deleteRepository: async ({ request: req, params }) => { + // const repositoryId = params.repositoryId + // const perms = await authUser(req, { repositoryId }) + + // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') + // if (!perms.projectPermissions) return new NotFound404() + // if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() + // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') + // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') + + // const body = await deleteRepository({ + // repositoryId, + // userId: perms.user.id, + // requestId: req.id, + // projectId: perms.projectId, + // }) + // if (body instanceof ErrorResType) return body + + // return { + // status: 204, + // body, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts index 41289f4b9..61eff61fb 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts @@ -1,90 +1,90 @@ -import type { AsyncReturnType } from '@cpn-console/shared' -import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared' -import { - listServiceChains as listServiceChainsBusiness, - getServiceChainDetails as getServiceChainDetailsBusiness, - retryServiceChain as retryServiceChainBusiness, - validateServiceChain as validateServiceChainBusiness, - getServiceChainFlows as getServiceChainFlowsBusiness, -} from './business' -import '@old-server/types/index' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { Forbidden403 } from '@old-server/utils/errors' - -export function serviceChainRouter() { - return serverInstance.router(serviceChainContract, { - listServiceChains: async ({ request: req }) => { - const { adminPermissions } = await authUser(req) - - let body: AsyncReturnType = [] - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listServiceChainsBusiness() - } - - return { - status: 200, - body, - } - }, - - getServiceChainDetails: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403() - - const serviceChainId = params.serviceChainId - const serviceChainDetails - = await getServiceChainDetailsBusiness(serviceChainId) - - return { - status: 200, - body: serviceChainDetails, - } - }, - - retryServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403() - - const serviceChainId = params.serviceChainId - await retryServiceChainBusiness(serviceChainId) - - return { - status: 204, - body: null, - } - }, - - validateServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403() - - const serviceChainId = params.validationId - await validateServiceChainBusiness(serviceChainId) - - return { - status: 204, - body: null, - } - }, - - getServiceChainFlows: async ({ params, request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403() - - const serviceChainId = params.serviceChainId - const serviceChainFlows - = await getServiceChainFlowsBusiness(serviceChainId) - - return { - status: 200, - body: serviceChainFlows, - } - }, - - }) -} +// import type { AsyncReturnType } from '@cpn-console/shared' +// import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared' +// import { + // listServiceChains as listServiceChainsBusiness, + // getServiceChainDetails as getServiceChainDetailsBusiness, + // retryServiceChain as retryServiceChainBusiness, + // validateServiceChain as validateServiceChainBusiness, + // getServiceChainFlows as getServiceChainFlowsBusiness, +// } from './business' +// import '@old-server/types/index' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { Forbidden403 } from '@old-server/utils/errors' + +// export function serviceChainRouter() { + // return serverInstance.router(serviceChainContract, { + // listServiceChains: async ({ request: req }) => { + // const { adminPermissions } = await authUser(req) + + // let body: AsyncReturnType = [] + // if (AdminAuthorized.isAdmin(adminPermissions)) { + // body = await listServiceChainsBusiness() + // } + + // return { + // status: 200, + // body, + // } + // }, + + // getServiceChainDetails: async ({ params, request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + // return new Forbidden403() + + // const serviceChainId = params.serviceChainId + // const serviceChainDetails + // = await getServiceChainDetailsBusiness(serviceChainId) + + // return { + // status: 200, + // body: serviceChainDetails, + // } + // }, + + // retryServiceChain: async ({ params, request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + // return new Forbidden403() + + // const serviceChainId = params.serviceChainId + // await retryServiceChainBusiness(serviceChainId) + + // return { + // status: 204, + // body: null, + // } + // }, + + // validateServiceChain: async ({ params, request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + // return new Forbidden403() + + // const serviceChainId = params.validationId + // await validateServiceChainBusiness(serviceChainId) + + // return { + // status: 204, + // body: null, + // } + // }, + + // getServiceChainFlows: async ({ params, request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) + // return new Forbidden403() + + // const serviceChainId = params.serviceChainId + // const serviceChainFlows + // = await getServiceChainFlowsBusiness(serviceChainId) + + // return { + // status: 200, + // body: serviceChainFlows, + // } + // }, + + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts index 77471d060..be17ad864 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts @@ -1,43 +1,43 @@ -import { AdminAuthorized, serviceContract } from '@cpn-console/shared' -import { checkServicesHealth, refreshServicesHealth } from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { Forbidden403 } from '@old-server/utils/errors' - -export function serviceMonitorRouter() { - return serverInstance.router(serviceContract, { - getServiceHealth: async () => { - const serviceData = checkServicesHealth() - - return { - status: 200, - body: serviceData, - } - }, - - getCompleteServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req) - - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - const serviceData = checkServicesHealth() - - return { - status: 200, - body: serviceData, - } - }, - - refreshServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - - await refreshServicesHealth() - const serviceData = checkServicesHealth() - - return { - status: 200, - body: serviceData, - } - }, - }) -} +// import { AdminAuthorized, serviceContract } from '@cpn-console/shared' +// import { checkServicesHealth, refreshServicesHealth } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { Forbidden403 } from '@old-server/utils/errors' + +// export function serviceMonitorRouter() { + // return serverInstance.router(serviceContract, { + // getServiceHealth: async () => { + // const serviceData = checkServicesHealth() + + // return { + // status: 200, + // body: serviceData, + // } + // }, + + // getCompleteServiceHealth: async ({ request: req }) => { + // const { adminPermissions } = await authUser(req) + + // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + // const serviceData = checkServicesHealth() + + // return { + // status: 200, + // body: serviceData, + // } + // }, + + // refreshServiceHealth: async ({ request: req }) => { + // const { adminPermissions } = await authUser(req) + // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + + // await refreshServicesHealth() + // const serviceData = checkServicesHealth() + + // return { + // status: 200, + // body: serviceData, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts index f71f0ee93..6606d8ef4 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts @@ -1,88 +1,88 @@ -import { AdminAuthorized, stageContract } from '@cpn-console/shared' -import { - createStage, - deleteStage, - getStageAssociatedEnvironments, - listStages, - updateStage, -} from './business' -import { serverInstance } from '@old-server/app' - -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' - -export function stageRouter() { - return serverInstance.router(stageContract, { - - // Récupérer les types d'environnement disponibles - listStages: async () => { - const body = await listStages() - - return { - status: 200, - body, - } - }, - - // Récupérer les environnements associés au stage - getStageEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const stageId = params.stageId - const body = await getStageAssociatedEnvironments(stageId) - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - // Créer un stage - createStage: async ({ request: req, body: data }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const body = await createStage(data) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - // Modifier une association stage / clusters - updateStage: async ({ request: req, params, body: data }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const stageId = params.stageId - - const body = await updateStage(stageId, data) - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - // Supprimer un stage - deleteStage: async ({ request: req, params }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const stageId = params.stageId - - const body = await deleteStage(stageId) - if (body instanceof ErrorResType) return body - - return { - status: 204, - body, - } - }, - }) -} +// import { AdminAuthorized, stageContract } from '@cpn-console/shared' +// import { + // createStage, + // deleteStage, + // getStageAssociatedEnvironments, + // listStages, + // updateStage, +// } from './business' +// import { serverInstance } from '@old-server/app' + +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' + +// export function stageRouter() { + // return serverInstance.router(stageContract, { + + // // Récupérer les types d'environnement disponibles + // listStages: async () => { + // const body = await listStages() + + // return { + // status: 200, + // body, + // } + // }, + + // // Récupérer les environnements associés au stage + // getStageEnvironments: async ({ request: req, params }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const stageId = params.stageId + // const body = await getStageAssociatedEnvironments(stageId) + // if (body instanceof ErrorResType) return body + + // return { + // status: 200, + // body, + // } + // }, + + // // Créer un stage + // createStage: async ({ request: req, body: data }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const body = await createStage(data) + // if (body instanceof ErrorResType) return body + + // return { + // status: 201, + // body, + // } + // }, + + // // Modifier une association stage / clusters + // updateStage: async ({ request: req, params, body: data }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const stageId = params.stageId + + // const body = await updateStage(stageId, data) + // if (body instanceof ErrorResType) return body + + // return { + // status: 200, + // body, + // } + // }, + + // // Supprimer un stage + // deleteStage: async ({ request: req, params }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const stageId = params.stageId + + // const body = await deleteStage(stageId) + // if (body instanceof ErrorResType) return body + + // return { + // status: 204, + // body, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts index df4179a47..b08186cdb 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts @@ -1,36 +1,36 @@ -import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared' -import { getPluginsConfig, updatePluginConfig } from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' +// import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared' +// import { getPluginsConfig, updatePluginConfig } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' -export function pluginConfigRouter() { - return serverInstance.router(systemPluginContract, { - // Récupérer les configurations plugins - getPluginsConfig: async ({ request: req }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() +// export function pluginConfigRouter() { + // return serverInstance.router(systemPluginContract, { + // // Récupérer les configurations plugins + // getPluginsConfig: async ({ request: req }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const services = await getPluginsConfig() + // const services = await getPluginsConfig() - return { - status: 200, - body: services, + // return { + // status: 200, + // body: services, - } - }, - // Mettre à jour les configurations plugins - updatePluginsConfig: async ({ request: req, body }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // } + // }, + // // Mettre à jour les configurations plugins + // updatePluginsConfig: async ({ request: req, body }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const resBody = await updatePluginConfig(body) - if (resBody instanceof ErrorResType) return resBody + // const resBody = await updatePluginConfig(body) + // if (resBody instanceof ErrorResType) return resBody - return { - status: 204, - body: resBody, - } - }, - }) -} + // return { + // status: 204, + // body: resBody, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts index 730dfe083..79c53c7a2 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts @@ -1,21 +1,21 @@ -import { systemContract } from '@cpn-console/shared' -import { serverInstance } from '@old-server/app' -import { appVersion } from '@old-server/utils/env' +// import { systemContract } from '@cpn-console/shared' +// import { serverInstance } from '@old-server/app' +// import { appVersion } from '@old-server/utils/env' -export function systemRouter() { - return serverInstance.router(systemContract, { - getVersion: async () => ({ - status: 200, - body: { - version: appVersion, - }, - }), +// export function systemRouter() { + // return serverInstance.router(systemContract, { + // getVersion: async () => ({ + // status: 200, + // body: { + // version: appVersion, + // }, + // }), - getHealth: async () => ({ - status: 200, - body: { - status: 'OK', - }, - }), - }) -} + // getHealth: async () => ({ + // status: 200, + // body: { + // status: 'OK', + // }, + // }), + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts index e1ed3a6a8..977248157 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts @@ -1,30 +1,30 @@ -import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared' -import { getSystemSettings, upsertSystemSetting } from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { Forbidden403 } from '@old-server/utils/errors' +// import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared' +// import { getSystemSettings, upsertSystemSetting } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { Forbidden403 } from '@old-server/utils/errors' -export function systemSettingsRouter() { - return serverInstance.router(systemSettingsContract, { - listSystemSettings: async ({ query }) => { - const systemSettings = await getSystemSettings(query.key) +// export function systemSettingsRouter() { + // return serverInstance.router(systemSettingsContract, { + // listSystemSettings: async ({ query }) => { + // const systemSettings = await getSystemSettings(query.key) - return { - status: 200, - body: systemSettings, - } - }, + // return { + // status: 200, + // body: systemSettings, + // } + // }, - upsertSystemSetting: async ({ request: req, body: data }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + // upsertSystemSetting: async ({ request: req, body: data }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - const systemSetting = await upsertSystemSetting(data) + // const systemSetting = await upsertSystemSetting(data) - return { - status: 201, - body: systemSetting, - } - }, - }) -} + // return { + // status: 201, + // body: systemSetting, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts index 42e03caef..d36a40a81 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts @@ -1,63 +1,63 @@ -import { AdminAuthorized, userContract } from '@cpn-console/shared' -import { - getMatchingUsers, - getUsers, - logViaSession, - patchUsers, -} from './business' -import '@old-server/types/index' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' - -export function userRouter() { - return serverInstance.router(userContract, { - getMatchingUsers: async ({ query }) => { - const usersMatching = await getMatchingUsers(query) - - return { - status: 200, - body: usersMatching, - } - }, - - auth: async ({ request: req }) => { - const user = req.session.user - - if (!user) return new Unauthorized401() - - const { user: body } = await logViaSession(user) - - return { - status: 200, - body, - } - }, - - getAllUsers: async ({ request: req, query: { relationType, ...query } }) => { - const perms = await authUser(req) - - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const body = await getUsers(query, relationType) - if (body instanceof ErrorResType) return body - - return { - status: 200, - body, - } - }, - - patchUsers: async ({ request: req, body }) => { - const perms = await authUser(req) - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - const users = await patchUsers(body) - - return { - status: 200, - body: users, - } - }, - }) -} +// import { AdminAuthorized, userContract } from '@cpn-console/shared' +// import { + // getMatchingUsers, + // getUsers, + // logViaSession, + // patchUsers, +// } from './business' +// import '@old-server/types/index' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' + +// export function userRouter() { + // return serverInstance.router(userContract, { + // getMatchingUsers: async ({ query }) => { + // const usersMatching = await getMatchingUsers(query) + + // return { + // status: 200, + // body: usersMatching, + // } + // }, + + // auth: async ({ request: req }) => { + // const user = req.session.user + + // if (!user) return new Unauthorized401() + + // const { user: body } = await logViaSession(user) + + // return { + // status: 200, + // body, + // } + // }, + + // getAllUsers: async ({ request: req, query: { relationType, ...query } }) => { + // const perms = await authUser(req) + + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const body = await getUsers(query, relationType) + // if (body instanceof ErrorResType) return body + + // return { + // status: 200, + // body, + // } + // }, + + // patchUsers: async ({ request: req, body }) => { + // const perms = await authUser(req) + // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() + + // const users = await patchUsers(body) + + // return { + // status: 200, + // body: users, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts index bc320b6d1..c24d8747b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts @@ -1,48 +1,48 @@ -import { personalAccessTokenContract } from '@cpn-console/shared' - -import '@old-server/types/index' -import { createToken, deleteToken, listTokens } from './business' -import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' - -export function personalAccessTokenRouter() { - return serverInstance.router(personalAccessTokenContract, { - listPersonalAccessTokens: async ({ request: req }) => { - const perms = await authUser(req) - - if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - const body = await listTokens(perms.user.id) - - return { - status: 200, - body, - } - }, - - createPersonalAccessToken: async ({ request: req, body: data }) => { - const perms = await authUser(req) - - if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - const body = await createToken(data, perms.user.id) - if (body instanceof ErrorResType) return body - - return { - status: 201, - body, - } - }, - - deletePersonalAccessToken: async ({ request: req, params }) => { - const perms = await authUser(req) - - if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - await deleteToken(params.tokenId, perms.user.id) - - return { - status: 204, - body: null, - } - }, - }) -} +// import { personalAccessTokenContract } from '@cpn-console/shared' + +// import '@old-server/types/index' +// import { createToken, deleteToken, listTokens } from './business' +// import { serverInstance } from '@old-server/app' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' + +// export function personalAccessTokenRouter() { + // return serverInstance.router(personalAccessTokenContract, { + // listPersonalAccessTokens: async ({ request: req }) => { + // const perms = await authUser(req) + + // if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + // const body = await listTokens(perms.user.id) + + // return { + // status: 200, + // body, + // } + // }, + + // createPersonalAccessToken: async ({ request: req, body: data }) => { + // const perms = await authUser(req) + + // if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + // const body = await createToken(data, perms.user.id) + // if (body instanceof ErrorResType) return body + + // return { + // status: 201, + // body, + // } + // }, + + // deletePersonalAccessToken: async ({ request: req, params }) => { + // const perms = await authUser(req) + + // if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() + // await deleteToken(params.tokenId, perms.user.id) + + // return { + // status: 204, + // body: null, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts index b55c3fcd9..ba1a45a3b 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts @@ -1,64 +1,64 @@ -import { AdminAuthorized, zoneContract } from '@cpn-console/shared' -import { createZone, deleteZone, listZones, updateZone } from './business' -import { serverInstance } from '@old-server/app' +// import { AdminAuthorized, zoneContract } from '@cpn-console/shared' +// import { createZone, deleteZone, listZones, updateZone } from './business' +// import { serverInstance } from '@old-server/app' -import { authUser } from '@old-server/utils/controller' -import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' +// import { authUser } from '@old-server/utils/controller' +// import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' -export function zoneRouter() { - return serverInstance.router(zoneContract, { - listZones: async () => { - const zones = await listZones() +// export function zoneRouter() { + // return serverInstance.router(zoneContract, { + // listZones: async () => { + // const zones = await listZones() - return { - status: 200, - body: zones, - } - }, + // return { + // status: 200, + // body: zones, + // } + // }, - createZone: async ({ request: req, body: data }) => { - const { user, adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user) return new Unauthorized401('Require to be requested from user not api key') + // createZone: async ({ request: req, body: data }) => { + // const { user, adminPermissions } = await authUser(req) + // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + // if (!user) return new Unauthorized401('Require to be requested from user not api key') - const body = await createZone(data, user.id, req.id) - if (body instanceof ErrorResType) return body + // const body = await createZone(data, user.id, req.id) + // if (body instanceof ErrorResType) return body - return { - status: 201, - body, - } - }, + // return { + // status: 201, + // body, + // } + // }, - updateZone: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user) return new Unauthorized401('Require to be requested from user not api key') + // updateZone: async ({ request: req, params, body: data }) => { + // const { user, adminPermissions } = await authUser(req) + // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + // if (!user) return new Unauthorized401('Require to be requested from user not api key') - const zoneId = params.zoneId + // const zoneId = params.zoneId - const body = await updateZone(zoneId, data, user.id, req.id) - if (body instanceof ErrorResType) return body + // const body = await updateZone(zoneId, data, user.id, req.id) + // if (body instanceof ErrorResType) return body - return { - status: 200, - body, - } - }, + // return { + // status: 200, + // body, + // } + // }, - deleteZone: async ({ request: req, params }) => { - const { user, adminPermissions } = await authUser(req) - if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - if (!user) return new Unauthorized401('Require to be requested from user not api key') - const zoneId = params.zoneId + // deleteZone: async ({ request: req, params }) => { + // const { user, adminPermissions } = await authUser(req) + // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() + // if (!user) return new Unauthorized401('Require to be requested from user not api key') + // const zoneId = params.zoneId - const body = await deleteZone(zoneId, user.id, req.id) - if (body instanceof ErrorResType) return body + // const body = await deleteZone(zoneId, user.id, req.id) + // if (body instanceof ErrorResType) return body - return { - status: 204, - body, - } - }, - }) -} + // return { + // status: 204, + // body, + // } + // }, + // }) +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts index ba161135f..f3e0af579 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts @@ -1,55 +1,55 @@ -import { randomUUID } from 'node:crypto' -import type { FastifyServerOptions } from 'fastify' -import type { generateOpenApi } from '@ts-rest/open-api' -import { swaggerUiPath } from '@cpn-console/shared' -import { loggerConf } from './logger' -import { - NODE_ENV, - appVersion, - keycloakClientId, - keycloakClientSecret, - keycloakRealm, - keycloakRedirectUri, -} from './env' -import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui' +// import { randomUUID } from 'node:crypto' +// import type { FastifyServerOptions } from 'fastify' +// import type { generateOpenApi } from '@ts-rest/open-api' +// import { swaggerUiPath } from '@cpn-console/shared' +// import { loggerConf } from './logger' +// import { + // NODE_ENV, + // appVersion, + // keycloakClientId, + // keycloakClientSecret, + // keycloakRealm, + // keycloakRedirectUri, +// } from './env' +// import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui' -export const fastifyConf: FastifyServerOptions = { - maxParamLength: 5000, - logger: loggerConf[NODE_ENV] ?? loggerConf.production, - genReqId: () => randomUUID(), -} +// export const fastifyConf: FastifyServerOptions = { + // maxParamLength: 5000, + // logger: loggerConf[NODE_ENV] ?? loggerConf.production, + // genReqId: () => randomUUID(), +// } -const externalDocs = { - description: 'External documentation.', - url: 'https://cloud-pi-native.fr', -} +// const externalDocs = { + // description: 'External documentation.', + // url: 'https://cloud-pi-native.fr', +// } -export const swaggerConf: Parameters[1] = { - info: { - title: 'Console Cloud Pi Native', - description: 'API de gestion des ressources Cloud Pi Native.', - version: appVersion, - }, +// export const swaggerConf: Parameters[1] = { + // info: { + // title: 'Console Cloud Pi Native', + // description: 'API de gestion des ressources Cloud Pi Native.', + // version: appVersion, + // }, - externalDocs, - servers: [ - { - url: keycloakRedirectUri, - }, - ], -} + // externalDocs, + // servers: [ + // { + // url: keycloakRedirectUri, + // }, + // ], +// } -export const swaggerUiConf: FastifySwaggerUiOptions = { - routePrefix: swaggerUiPath, - uiConfig: { - docExpansion: 'list', - deepLinking: false, - }, - initOAuth: { - clientId: keycloakClientId, - clientSecret: keycloakClientSecret, - realm: keycloakRealm, - appName: 'Cloud Pi Native', - scopes: 'openid generic', - }, -} +// export const swaggerUiConf: FastifySwaggerUiOptions = { + // routePrefix: swaggerUiPath, + // uiConfig: { + // docExpansion: 'list', + // deepLinking: false, + // }, + // initOAuth: { + // clientId: keycloakClientId, + // clientSecret: keycloakClientSecret, + // realm: keycloakRealm, + // appName: 'Cloud Pi Native', + // scopes: 'openid generic', + // }, +// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts index 81eda4980..f5a40f0be 100644 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts +++ b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts @@ -131,7 +131,7 @@ export function getUserMockInfos(isAdmin: boolean, user?: UserDetails, project?: export function getUserMockInfos(isAdmin: boolean, user = getRandomRequestor(), project?: utilsController.ProjectPermState): utilsController.UserProfile | utilsController.UserProjectProfile { return { adminPermissions: isAdmin ? 2n : 0n, - user, + user: user as UserDetails, ...project, } } diff --git a/apps/server-nestjs/src/main.ts b/apps/server-nestjs/src/main.ts index d0c24014c..d455db402 100644 --- a/apps/server-nestjs/src/main.ts +++ b/apps/server-nestjs/src/main.ts @@ -6,6 +6,6 @@ import { MainModule } from './main.module'; async function bootstrap() { const app = await NestFactory.create(MainModule, { bufferLogs: true }); app.useLogger(app.get(Logger)); - await app.listen(process.env.PORT ?? 8080); + await app.listen(process.env.PORT ?? 0); } bootstrap(); From 7d41ca9b55070f3ceb8ced13fa995a44dba9f0f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 22 Dec 2025 14:30:30 +0100 Subject: [PATCH 27/33] chore(server-nestjs): delete unneeded code --- .../fastify/fastify.service.spec.ts | 18 ------------------ .../infrastructure/fastify/fastify.service.ts | 4 ---- .../infrastructure/infrastructure.module.ts | 3 --- 3 files changed, 25 deletions(-) delete mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.ts diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.spec.ts deleted file mode 100644 index 6c473f5b1..000000000 --- a/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { FastifyService } from './fastify.service'; - -describe('FastifyService', () => { - let service: FastifyService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [FastifyService], - }).compile(); - - service = module.get(FastifyService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.ts deleted file mode 100644 index 7f662a41f..000000000 --- a/apps/server-nestjs/src/cpin-module/infrastructure/fastify/fastify.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class FastifyService {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts index 86cd80c5c..fcd00fad9 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { ConfigurationModule } from './configuration/configuration.module'; import { DatabaseService } from './database/database.service'; -import { FastifyService } from './fastify/fastify.service'; import { HttpClientService } from './http-client/http-client.service'; import { LoggerModule } from './logger/logger.module'; import { ServerService } from './server/server.service'; @@ -11,14 +10,12 @@ import { ServerService } from './server/server.service'; providers: [ DatabaseService, HttpClientService, - FastifyService, ServerService, ], imports: [LoggerModule, ConfigurationModule], exports: [ DatabaseService, HttpClientService, - FastifyService, ServerService, ], }) From c6098d3c57fd4bc8857e039a3dceceedbd96ac2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 22 Dec 2025 14:30:50 +0100 Subject: [PATCH 28/33] chore(server-nestjs): update README with a plan for the future --- apps/server-nestjs/README.md | 86 +++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 12 deletions(-) diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md index 9f55b1ee3..eface5606 100644 --- a/apps/server-nestjs/README.md +++ b/apps/server-nestjs/README.md @@ -2,7 +2,21 @@ Ce dossier contient une nouvelle version de `server`, basée sur NestJS. -On va profiter de cette nouvelle mouture pour passer de ça : +## Objectifs + +Rappel : L'objectif principal de cette démarche est de préparer l'extraction de +chaque "plugin" en tant que module NestJS incluant à la fois la partie Front et +la partie Backend pour une meilleure composition de la Console. + +On va profiter de cette vision pour passer, côté `server` d'un "Back For +Front", à un backend qui va gérer à la fois le Front et le Back de différentes +fonctionnalités (appelée "Plugins"), et ce parfois de manière dynamique (mais +déjà en statique, ce sera pas mal 😅). + + +## Conséquences pour apps/server + +On va donc passer de ça : ```mermaid flowchart TD @@ -228,7 +242,7 @@ flowchart TD Dots --> LoggerService ``` -To update `old-server` (after rebasing on `origin/master`, for instance) : +Pour mettre à jour `old-server` (après avoir rebasé sur `origin/master`, par exemple) : ```bash server-nestjs/$ rm -rf src/cpin-module/old-server @@ -237,17 +251,65 @@ server-nestjs/$ find src/cpin-module/old-server -type f -iname "*.ts" -exec sed server-nestjs/$ find src/cpin-module/old-server -type f -iname "*.ts" -exec sed -i -e "s#\.[jt]s'#'#g" {} \; ``` -## To delete (once we have a sastifying nestjs implementation): +## Fichiers à supprimer dans le futur + +Certains fichiers de `old-server` servait de "framework" pour le backend, et +vont donc être réécrits en tant que modules/services NestJS. On va garder la +liste ici, ce qui permettra de ne pas être constamment en conflit sur le code +de `server`. En attendant de pouvoir s'en débarrasser, et afin de s'assurer que +leur code n'est pas utilisé dans d'autres parties du backend, on va commenter +l'intégralité de ces fichiers (comme ça pas d'erreur d'import quand on les +supprimera). -Some `old-server` files are being rewritten and incorporated as NestJS modules. -We will keep track of them here so that we can go back and forth between the previous -implementation and the future NestJS one. In the meantime their code is commented out -in order to show if they can be safely removed (no import errors elsewhere) + +Voilà donc la liste des fichiers "dépréciés" : ``` -old-server/src/utils/logger.ts -> Replaced by LoggerModule -old-server/src/utils/env.ts -> Replaced by ConfigurationModule -old-server/src/init/db/* (except dump.ts) -> Replaced by DatabaseInitializationService -old-server/src/connect.ts -> Replaced by DatabaseService -old-server/src/server.ts -> Incorporated in ApplicationInitializationService +old-server/src/app.ts -> Réécrit en AppService +old-server/src/connect.ts -> Réécrit en DatabaseService +old-server/src/init/db/* (à part dump.ts) -> Réécrit en DatabaseInitializationService +old-server/src/resources/**/router.ts -> Réécrit en **RouterService +old-server/src/resources/index.ts -> Réécrit en RouterService +old-server/src/server.ts -> Intégré à ApplicationInitializationService +old-server/src/utils/env.ts -> Réécrit en ConfigurationModule +old-server/src/utils/fastify.ts -> Réécrit en FastifyService +old-server/src/utils/keycloak-utils.ts -> Intégré dans AppService +old-server/src/utils/keycloak.ts -> Intégré dans AppService +old-server/src/utils/logger.ts -> Réécrit en LoggerModule +old-server/src/utils/plugin.ts -> Réécrit en PluginManagementService ``` + +## Prochaines itérations sur le sujet + +Tâches à réaliser par la suite dans d'autres itérations/tickets/etc. : + +- Migrer une fonctionnalité "verticale" complète (Route, Contract, Controller, + Business, Queries, Prisma schema) dans son propre module NestJS qui sera + importé dans `MainModule` (et pas dans `CpinModule` qui devra disparaître + à terme). +- Définir la liste de ces fonctionnalités verticales, et planifier l'extraction + de certaines d'entre elles (OpenCDS, typiquement, qui n'a rien à faire dans + le code de base de la Console) +- Migrer la base actuelle de NestJS de Jest vers Vitest et s'assurer que les + tests unitaires passent à nouveaux (attention, certains d'entre eux devront + être adaptés vu qu'on a commencé à réécrire du code au standard NestJS) +- Intérgrer `server-nestjs` dans tous les `docker compose` et les différents + scripts PNPM/Bash qui font tout le sel de nos process de dev +- Revoir les imports de données (le fameux `dump.ts`). C'était déjà une + mauvaise idée à l'époque, ça l'est encore plus aujourd'hui. On ferait mieux + d'utiliser un side-container pour ça +- Être capable de déployer `server-nestjs` en parallèle de `server`, avec + probablement un reverse proxy dédié à la migration des routes de l'ancien + vers le nouveau (à rediscuter plus concrètement) + +Les étapes d'après-après (quand on sera sereins sur la migration de `server` +vers `server-nestjs`) : + +- Tester les capacités de NestJS SSR (Server Side Rendering), notamment + vis-à-vis de nos composants VueJS. Il y aura probablement des sujets autour + de l'isolation du code VueJS des différentes fonctionnalités afin de + faciliter leur extraction de `client` vers `server` +- Implémenter une fonctionnalité du Front en tant que module NestJS SSR + (OpenCDS est un très bon cas d'étude grâce à son côté très isolé dans le + code). Idéalement ce code "Front" ajouté "côté Backend" devra être colocalisé + avec le code "Backend" correspondant. From 8f517e11ad585425b5b2a468cdf8bb4c14497bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 23 Dec 2025 12:06:32 +0100 Subject: [PATCH 29/33] chore(server-nestjs): update README with the modularization target --- apps/server-nestjs/README.md | 129 +++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/apps/server-nestjs/README.md b/apps/server-nestjs/README.md index eface5606..5fbfdeaf8 100644 --- a/apps/server-nestjs/README.md +++ b/apps/server-nestjs/README.md @@ -242,6 +242,135 @@ flowchart TD Dots --> LoggerService ``` +et enfin à ça : + +```mermaid +flowchart TD + + %% --- Top-level Nest module --- + NestJS["Point d'entrée de NestJS"] + MainModule["MainModule"] + + NestJS --> MainModule + + subgraph CoreModule["CoreModule"] + ApplicationInitializationService["ApplicationInitializationService"] + end + + subgraph InfrastructureModule["InfrastructureModule"] + LoggerService["LoggerService"] + ConfigurationService["ConfigurationService"] + DatabaseService["DatabaseService"] + HTTPClientService["HTTPClientService"] + end + + OtherAPIService["APIs externes
(par ex. OpenCDS)"] + HTTPClientService --> OtherAPIService + + subgraph MandatoryModules["Modules obligatoires CPiN"] + subgraph GitlabModule["GitlabModule"] + GitlabController["GitlabController"] + GitlabBusinessService["GitlabBusinessService"] + GitlabDTOService["GitlabDTOService"] + GitlabController --> GitlabBusinessService + GitlabController --> LoggerService + GitlabBusinessService --> GitlabDTOService + GitlabBusinessService --> LoggerService + GitlabDTOService --> DatabaseService + GitlabDTOService --> LoggerService + end + + subgraph ArgoCDModule["ArgoCDModule"] + ArgoCDController["ArgoCDController"] + ArgoCDBusinessService["ArgoCDBusinessService"] + ArgoCDDTOService["ArgoCDDTOService"] + ArgoCDController --> ArgoCDBusinessService + ArgoCDController --> LoggerService + ArgoCDBusinessService --> ArgoCDDTOService + ArgoCDBusinessService --> LoggerService + ArgoCDDTOService --> DatabaseService + ArgoCDDTOService --> LoggerService + end + + subgraph KubernetesModule["KubernetesModule"] + KubernetesController["KubernetesController"] + KubernetesBusinessService["KubernetesBusinessService"] + KubernetesDTOService["KubernetesDTOService"] + KubernetesController --> KubernetesBusinessService + KubernetesController --> LoggerService + KubernetesBusinessService --> KubernetesDTOService + KubernetesBusinessService --> LoggerService + KubernetesDTOService --> DatabaseService + KubernetesDTOService --> LoggerService + end + + subgraph AdminRoleModule["AdminRoleModule"] + AdminRoleController["AdminRoleController"] + AdminRoleBusinessService["AdminRoleBusinessService"] + AdminRoleDTOService["AdminRoleDTOService"] + AdminRoleController --> AdminRoleBusinessService + AdminRoleController --> LoggerService + AdminRoleBusinessService --> AdminRoleDTOService + AdminRoleBusinessService --> LoggerService + AdminRoleDTOService --> LoggerService + AdminRoleDTOService --> DatabaseService + end + + subgraph AdminTokenModule["AdminTokenModule"] + AdminTokenController["AdminTokenController"] + AdminTokenBusinessService["AdminTokenBusinessService"] + AdminTokenDTOService["AdminTokenDTOService"] + AdminTokenController --> AdminTokenBusinessService + AdminTokenController --> LoggerService + AdminTokenBusinessService --> AdminTokenDTOService + AdminTokenBusinessService --> LoggerService + AdminTokenDTOService --> DatabaseService + AdminTokenDTOService --> LoggerService + end + + subgraph ClusterModule["ClusterModule"] + ClusterController["ClusterController"] + ClusterBusinessService["ClusterBusinessService"] + ClusterDTOService["ClusterDTOService"] + ClusterController --> ClusterBusinessService + ClusterController --> LoggerService + ClusterBusinessService --> ClusterDTOService + ClusterBusinessService --> LoggerService + ClusterDTOService --> DatabaseService + ClusterDTOService --> LoggerService + end + OtherBusinessModules["...Other Business Modules"] + end + + CoreModule --> GitlabModule + CoreModule --> ArgoCDModule + CoreModule --> KubernetesModule + CoreModule --> AdminRoleModule + CoreModule --> AdminTokenModule + CoreModule --> ClusterModule + + subgraph ThirdPartyModules["Modules optionnels de CPin"] + subgraph ServiceChainModule["ServiceChainModule"] + ServiceChainController["ServiceChainController"] + ServiceChainBusinessService["ServiceChainBusinessService"] + ServiceChainController --> ServiceChainBusinessService + ServiceChainController --> LoggerService + ServiceChainBusinessService --> HTTPClientService + ServiceChainBusinessService --> LoggerService + end + end + + CoreModule --> ServiceChainModule + + %% --- Module wiring --- + MainModule --> ApplicationInitializationService + + %% Application initialization + ApplicationInitializationService --> LoggerService + ApplicationInitializationService --> ConfigurationService + ApplicationInitializationService --> LoggerService +``` + Pour mettre à jour `old-server` (après avoir rebasé sur `origin/master`, par exemple) : ```bash From d4ebfcdd130175b610b3e05740c51af10e6fadd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 6 Jan 2026 10:20:59 +0100 Subject: [PATCH 30/33] chore(server-nestjs): remove now useless old-server directory We will now begin to create dedicated modules taken from ../server --- .../src/cpin-module/old-server/.env-example | 18 - .../old-server/.env.docker-example | 13 - .../cpin-module/old-server/.env.integ-example | 43 -- .../src/cpin-module/old-server/Dockerfile | 66 --- .../src/cpin-module/old-server/README.md | 39 -- .../cpin-module/old-server/eslint.config.js | 3 - .../src/cpin-module/old-server/migrate-db.sh | 77 --- .../src/cpin-module/old-server/nodemon.json | 4 - .../cpin-module/old-server/prisma.config.ts | 9 - .../old-server/src/__mocks__/prisma.ts | 14 - .../src/__mocks__/utils/hook-wrapper.ts | 34 -- .../cpin-module/old-server/src/app.spec.ts | 21 - .../src/cpin-module/old-server/src/app.ts | 59 --- .../old-server/src/connect.spec.ts | 61 --- .../src/cpin-module/old-server/src/connect.ts | 52 --- .../old-server/src/init/db/dump.ts | 28 -- .../old-server/src/init/db/index.ts | 51 -- .../old-server/src/init/db/utils.spec.ts | 52 --- .../old-server/src/init/db/utils.ts | 85 ---- .../old-server/src/mocks/prisma.ts | 14 - .../cpin-module/old-server/src/mocks/utils.ts | 24 - .../src/cpin-module/old-server/src/plugins.ts | 46 -- .../old-server/src/prepare-app.spec.ts | 70 --- .../cpin-module/old-server/src/prepare-app.ts | 106 ----- .../src/cpin-module/old-server/src/prisma.ts | 5 - .../20230706084346_dso/migration.sql | 151 ------ .../20230710181052_dso/migration.sql | 85 ---- .../20230711132934_dso/migration.sql | 11 - .../20230802143822_dso/migration.sql | 10 - .../20230912084459_dso/migration.sql | 2 - .../20231010111515_dso/migration.sql | 8 - .../20231011125838_dso/migration.sql | 81 ---- .../20231011125839_dso/migration.sql | 35 -- .../20231011125841_dso/migration.sql | 36 -- .../20231012105520_dso/migration.sql | 11 - .../20231024155020_dso/migration.sql | 3 - .../20231026150220_dso/migration.sql | 3 - .../20240112135751_dso/migration.sql | 2 - .../20240321123436_dso/migration.sql | 12 - .../20240329172938_dso/migration.sql | 46 -- .../20240424093852_dso/migration.sql | 23 - .../20240427181037_dso/migration.sql | 19 - .../20240605135052_dso/migration.sql | 9 - .../20240612123132_dso/migration.sql | 8 - .../20240614222908_dso/migration.sql | 11 - .../20240618112205_dso/migration.sql | 58 --- .../20240717084709_dso/migration.sql | 9 - .../20240723135420_dso/migration.sql | 198 -------- .../20240725162050_dso/migration.sql | 5 - .../20240726210139_dso/migration.sql | 14 - .../20240808082632_dso/migration.sql | 17 - .../20240826143230_dso/migration.sql | 3 - .../20240829085548_dso/migration.sql | 12 - .../20240916141253_token/migration.sql | 23 - .../migration.sql | 8 - .../20240923142722_dso/migration.sql | 2 - .../20240923155416_dso/migration.sql | 2 - .../20240928002900_dso/migration.sql | 2 - .../migration.sql | 12 - .../20241104232540_add_usertype/migration.sql | 12 - .../20241104232541_add_pat/migration.sql | 84 ---- .../migration.sql | 2 - .../20241112101945_add_slug/migration.sql | 14 - .../migration.sql | 2 - .../20241216131342_dso/migration.sql | 17 - .../20250107104749_dso/migration.sql | 2 - .../migration.sql | 25 - .../migration.sql | 15 - .../20250723141246_dso/migration.sql | 2 - .../20250818095032_remove_quota/migration.sql | 44 -- .../migration.sql | 5 - .../migration.sql | 9 - .../migration.sql | 4 - .../migration.sql | 4 - .../src/prisma/migrations/migration_lock.toml | 3 - .../old-server/src/prisma/schema/admin.prisma | 20 - .../src/prisma/schema/project.prisma | 106 ----- .../src/prisma/schema/schema.prisma | 21 - .../old-server/src/prisma/schema/token.prisma | 30 -- .../src/prisma/schema/topography.prisma | 53 --- .../old-server/src/prisma/schema/user.prisma | 23 - .../src/resources/admin-role/business.spec.ts | 183 -------- .../src/resources/admin-role/business.ts | 90 ---- .../src/resources/admin-role/queries.ts | 32 -- .../src/resources/admin-role/router.spec.ts | 181 ------- .../src/resources/admin-role/router.ts | 74 --- .../resources/admin-token/business.spec.ts | 73 --- .../src/resources/admin-token/business.ts | 68 --- .../src/resources/admin-token/router.spec.ts | 161 ------- .../src/resources/admin-token/router.ts | 44 -- .../src/resources/cluster/business.spec.ts | 173 ------- .../src/resources/cluster/business.ts | 230 --------- .../src/resources/cluster/queries.ts | 312 ------------- .../src/resources/cluster/router.spec.ts | 311 ------------- .../src/resources/cluster/router.ts | 125 ----- .../resources/environment/business.spec.ts | 353 -------------- .../src/resources/environment/business.ts | 300 ------------ .../src/resources/environment/queries.ts | 98 ---- .../src/resources/environment/router.spec.ts | 372 --------------- .../src/resources/environment/router.ts | 109 ----- .../old-server/src/resources/index.ts | 49 -- .../src/resources/log/business.spec.ts | 42 -- .../old-server/src/resources/log/business.ts | 13 - .../old-server/src/resources/log/queries.ts | 57 --- .../src/resources/log/router.spec.ts | 93 ---- .../old-server/src/resources/log/router.ts | 32 -- .../src/resources/project-member/business.ts | 60 --- .../src/resources/project-member/queries.ts | 33 -- .../resources/project-member/router.spec.ts | 294 ------------ .../src/resources/project-member/router.ts | 82 ---- .../resources/project-role/business.spec.ts | 195 -------- .../src/resources/project-role/business.ts | 77 --- .../src/resources/project-role/queries.ts | 54 --- .../src/resources/project-role/router.spec.ts | 316 ------------- .../src/resources/project-role/router.ts | 90 ---- .../src/resources/project-service/business.ts | 95 ---- .../src/resources/project-service/queries.ts | 54 --- .../resources/project-service/router.spec.ts | 160 ------- .../src/resources/project-service/router.ts | 38 -- .../src/resources/project/business.spec.ts | 361 -------------- .../src/resources/project/business.ts | 409 ---------------- .../src/resources/project/queries.ts | 371 --------------- .../src/resources/project/router.spec.ts | 440 ------------------ .../src/resources/project/router.ts | 199 -------- .../old-server/src/resources/queries-index.ts | 14 - .../src/resources/repository/business.ts | 115 ----- .../src/resources/repository/queries.ts | 62 --- .../src/resources/repository/router.spec.ts | 402 ---------------- .../src/resources/repository/router.ts | 138 ------ .../resources/service-chain/business.spec.ts | 171 ------- .../src/resources/service-chain/business.ts | 27 -- .../src/resources/service-chain/queries.ts | 58 --- .../resources/service-chain/router.spec.ts | 306 ------------ .../src/resources/service-chain/router.ts | 90 ---- .../src/resources/service-monitor/business.ts | 9 - .../resources/service-monitor/router.spec.ts | 78 ---- .../src/resources/service-monitor/router.ts | 43 -- .../src/resources/stage/business.spec.ts | 113 ----- .../src/resources/stage/business.ts | 97 ---- .../old-server/src/resources/stage/queries.ts | 111 ----- .../src/resources/stage/router.spec.ts | 202 -------- .../old-server/src/resources/stage/router.ts | 88 ---- .../resources/system/config/business.spec.ts | 22 - .../src/resources/system/config/business.ts | 50 -- .../src/resources/system/config/queries.ts | 28 -- .../resources/system/config/router.spec.ts | 96 ---- .../src/resources/system/config/router.ts | 36 -- .../old-server/src/resources/system/index.ts | 1 - .../src/resources/system/router.spec.ts | 25 - .../old-server/src/resources/system/router.ts | 21 - .../src/resources/system/settings/business.ts | 9 - .../src/resources/system/settings/queries.ts | 18 - .../resources/system/settings/router.spec.ts | 67 --- .../src/resources/system/settings/router.ts | 30 -- .../src/resources/user/business.spec.ts | 222 --------- .../old-server/src/resources/user/business.ts | 201 -------- .../old-server/src/resources/user/queries.ts | 60 --- .../src/resources/user/router.spec.ts | 139 ------ .../old-server/src/resources/user/router.ts | 63 --- .../src/resources/user/tokens/business.ts | 51 -- .../src/resources/user/tokens/router.ts | 48 -- .../src/resources/zone/business.spec.ts | 133 ------ .../old-server/src/resources/zone/business.ts | 78 ---- .../old-server/src/resources/zone/queries.ts | 21 - .../src/resources/zone/router.spec.ts | 162 ------- .../old-server/src/resources/zone/router.ts | 64 --- .../cpin-module/old-server/src/server.spec.ts | 57 --- .../src/cpin-module/old-server/src/server.ts | 44 -- .../old-server/src/utils/business.ts | 41 -- .../old-server/src/utils/controller.ts | 169 ------- .../old-server/src/utils/date.spec.ts | 15 - .../cpin-module/old-server/src/utils/date.ts | 5 - .../cpin-module/old-server/src/utils/env.ts | 57 --- .../old-server/src/utils/errors.ts | 48 -- .../old-server/src/utils/fastify.ts | 55 --- .../old-server/src/utils/hook-wrapper.spec.ts | 235 ---------- .../old-server/src/utils/hook-wrapper.ts | 231 --------- .../src/utils/keycloak-utils.spec.ts | 45 -- .../old-server/src/utils/keycloak-utils.ts | 27 -- .../old-server/src/utils/keycloak.ts | 42 -- .../old-server/src/utils/logger.ts | 97 ---- .../cpin-module/old-server/src/utils/mocks.ts | 152 ------ .../old-server/src/utils/plugins.ts | 9 - .../old-server/src/utils/proxy.spec.ts | 157 ------- .../cpin-module/old-server/src/utils/proxy.ts | 78 ---- .../src/utils/queries-tools.spec.ts | 47 -- .../old-server/src/utils/queries-tools.ts | 11 - .../old-server/src/utils/random.spec.ts | 148 ------ .../old-server/vite.config.ts.backup | 18 - .../old-server/vitest-init.ts.backup | 11 - .../old-server/vitest.config.ts.backup | 34 -- 191 files changed, 14927 deletions(-) delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/.env-example delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/.env.docker-example delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/.env.integ-example delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/Dockerfile delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/README.md delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/eslint.config.js delete mode 100755 apps/server-nestjs/src/cpin-module/old-server/migrate-db.sh delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/nodemon.json delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/app.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/connect.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230706084346_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230710181052_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230711132934_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230802143822_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230912084459_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231010111515_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125838_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125839_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125841_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231012105520_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231024155020_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231026150220_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240112135751_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240321123436_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240329172938_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240424093852_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240427181037_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240605135052_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240612123132_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240614222908_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240618112205_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240717084709_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240723135420_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240725162050_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240726210139_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240808082632_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240826143230_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240829085548_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240916141253_token/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240919122331_optional_user_id/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923142722_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923155416_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240928002900_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241008125724_enabling_maven/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232540_add_usertype/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232541_add_pat/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241107142721_user_last_login/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112101945_add_slug/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241216131342_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250107104749_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222954_drop_organization/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250723141246_dso/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250818095032_remove_quota/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250916134454_add_project_resources/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/migration_lock.toml delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/admin.prisma delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/schema.prisma delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/token.prisma delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/topography.prisma delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/user.prisma delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/server.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/vite.config.ts.backup delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts.backup delete mode 100644 apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts.backup diff --git a/apps/server-nestjs/src/cpin-module/old-server/.env-example b/apps/server-nestjs/src/cpin-module/old-server/.env-example deleted file mode 100644 index 84236efbe..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/.env-example +++ /dev/null @@ -1,18 +0,0 @@ -DEV_SETUP="true" -NODE_ENV=development -# HOME=/home/node -SESSION_SECRET=a-very-strong-secret-with-more-than-32-char -KEYCLOAK_DOMAIN=localhost:8090 -KEYCLOAK_REALM=cloud-pi-native -KEYCLOAK_PROTOCOL=http -KEYCLOAK_CLIENT_ID=dso-console-backend -KEYCLOAK_CLIENT_SECRET=client-secret-backend -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -SERVER_PORT=4000 -DB_URL=postgresql://admin:admin@localhost:5432/dso-console-db?schema=public -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr - -# Configuration OpenCDS -OPENCDS_URL= -OPENCDS_API_TOKEN=token -OPENCDS_API_TLS_REJECT_UNAUTHORIZED=true diff --git a/apps/server-nestjs/src/cpin-module/old-server/.env.docker-example b/apps/server-nestjs/src/cpin-module/old-server/.env.docker-example deleted file mode 100644 index 0da54a8e4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/.env.docker-example +++ /dev/null @@ -1,13 +0,0 @@ -DOCKER=true -DEV_SETUP="true" -NODE_ENV=development -SESSION_SECRET=a-very-strong-secret-with-more-than-32-char -KEYCLOAK_DOMAIN=keycloak:8080 -KEYCLOAK_REALM=cloud-pi-native -KEYCLOAK_PROTOCOL=http -KEYCLOAK_CLIENT_ID=dso-console-backend -KEYCLOAK_CLIENT_SECRET=client-secret-backend -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -SERVER_PORT=8080 -DB_URL=postgresql://admin:admin@postgres:5432/dso-console-db?schema=public -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr diff --git a/apps/server-nestjs/src/cpin-module/old-server/.env.integ-example b/apps/server-nestjs/src/cpin-module/old-server/.env.integ-example deleted file mode 100644 index 33e23e778..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/.env.integ-example +++ /dev/null @@ -1,43 +0,0 @@ -DEV_SETUP="false" -INTEGRATION=true -KEYCLOAK_PROTOCOL=https -KEYCLOAK_CLIENT_ID= -KEYCLOAK_CLIENT_SECRET= -KEYCLOAK_DOMAIN= -KEYCLOAK_REALM= -ARGO_NAMESPACE= -ARGOCD_URL= -GITLAB_TOKEN= -GITLAB_URL= -HARBOR_ADMIN= -HARBOR_ADMIN_PASSWORD= -HARBOR_URL= -KEYCLOAK_ADMIN= -KEYCLOAK_ADMIN_PASSWORD= -KEYCLOAK_URL= -NEXUS_ADMIN= -NEXUS_ADMIN_PASSWORD= -NEXUS_URL= -PROJECTS_ROOT_DIR= -SONAR_API_TOKEN= -SONARQUBE_URL= -VAULT_TOKEN= -VAULT_URL= - -KUBECONFIG_HOST_PATH= -KUBECONFIG_PATH=$HOME/.kube/config -KUBECONFIG_CTX= - -EXTERNAL_PLUGINS_DIR_HOST_PATH=/path/to/plugins - -# Variables de plugins externes - -# GRAVITEE_APIM_API_ID= -# GRAVITEE_APIM_PLAN_ID= -# GRAVITEE_APIM_TOKEN= -# GRAVITEE_APIM_URL= -# GRAVITEE_GATEWAY_URL= - -# HTTP_PROXY= -# HTTPS_PROXY= -# NO_PROXY= diff --git a/apps/server-nestjs/src/cpin-module/old-server/Dockerfile b/apps/server-nestjs/src/cpin-module/old-server/Dockerfile deleted file mode 100644 index 71c0b3c02..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -# Base stage ----------------------------------------------------------------------- -FROM docker.io/node:22.14.0-bullseye-slim AS dev - -WORKDIR /app - -COPY --chown=node:root package.json ./ - -# Install pnpm version defined in package.json "packageManager" property -RUN npm install --global corepack@latest && corepack enable && corepack enable pnpm - -COPY --chown=node:root pnpm-workspace.yaml pnpm-lock.yaml .npmrc turbo.json ./ -COPY --chown=node:root patches ./patches -COPY --chown=node:root apps/server/package.json ./apps/server/package.json - -COPY --chown=node:root packages/eslintconfig/package.json ./packages/eslintconfig/package.json -COPY --chown=node:root packages/shared/package.json ./packages/shared/package.json -COPY --chown=node:root packages/hooks/package.json ./packages/hooks/package.json -COPY --chown=node:root packages/test-utils/package.json ./packages/test-utils/package.json -COPY --chown=node:root packages/tsconfig/package.json ./packages/tsconfig/package.json - -COPY --chown=node:root plugins/argocd/package.json ./plugins/argocd/package.json -COPY --chown=node:root plugins/gitlab/package.json ./plugins/gitlab/package.json -COPY --chown=node:root plugins/harbor/package.json ./plugins/harbor/package.json -COPY --chown=node:root plugins/keycloak/package.json ./plugins/keycloak/package.json -COPY --chown=node:root plugins/kubernetes/package.json ./plugins/kubernetes/package.json -COPY --chown=node:root plugins/nexus/package.json ./plugins/nexus/package.json -COPY --chown=node:root plugins/sonarqube/package.json ./plugins/sonarqube/package.json -COPY --chown=node:root plugins/vault/package.json ./plugins/vault/package.json - -RUN pnpm install --frozen-lockfile -COPY --chown=node:root plugins/ ./plugins/ -COPY --chown=node:root packages/ ./packages/ - -COPY --chown=node:root apps/server/ ./apps/server/ -# Generate Prisma client -RUN pnpm --filter server run db:generate -ENTRYPOINT [ "pnpm", "--filter", "server", "run" ] -CMD [ "dev" ] - - -# Build stage ---------------------------------------------------------------------- -FROM dev AS build - -# Build @cpn-console/server console-related dependencies -RUN pnpm run build -# Build @cpn-console/server -RUN pnpm --filter @cpn-console/server run build -# Export @cpn-console/server to target build directory -RUN pnpm --filter @cpn-console/server --prod deploy build - - -# Prod stage ----------------------------------------------------------------------- -FROM docker.io/node:22.14.0-bullseye-slim AS prod - -ARG APP_VERSION -ENV APP_VERSION=$APP_VERSION -VOLUME [ "/plugins" ] -WORKDIR /app -RUN mkdir -p /home/node/logs && chmod 770 -R /home/node/logs \ - && mkdir -p /home/node/.npm && chmod 770 -R /home/node/.npm \ - && chown node:root /app -COPY --chown=node:root --from=build /app/build . -RUN npm run db:generate -USER node -EXPOSE 8080 -ENTRYPOINT ["npm", "start"] diff --git a/apps/server-nestjs/src/cpin-module/old-server/README.md b/apps/server-nestjs/src/cpin-module/old-server/README.md deleted file mode 100644 index 37b89ac99..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Console Cloud π Native - Serveur - -## Installation - -```sh -npm install -``` - -## Lancement de l'app - -```sh -npm run dev -``` - -## Lancement des tests unitaires - -```sh -npm run test -``` - -## Formattage du code - -```sh -# Lister les problèmes de formatage -npm run lint - -# Régler automatiquement les problèmes de formatage -npm run format -``` - -## Lancement de l'app en mode production - -```sh -npm start -``` - -## Activation OpenCDS - -Se référer à [la documentation dédiée](../../packages/opencds/README.adoc) diff --git a/apps/server-nestjs/src/cpin-module/old-server/eslint.config.js b/apps/server-nestjs/src/cpin-module/old-server/eslint.config.js deleted file mode 100644 index 5a664d2b5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import eslintConfigBase from '@cpn-console/eslint-config' - -export default eslintConfigBase diff --git a/apps/server-nestjs/src/cpin-module/old-server/migrate-db.sh b/apps/server-nestjs/src/cpin-module/old-server/migrate-db.sh deleted file mode 100755 index a65db96cb..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/migrate-db.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -set -e - -# Colorize terminal -red='\e[0;31m' -no_color='\033[0m' - -# Default -RESET_DB="false" - -# DB Values -DB_NAME=dso-console-db -DB_PORT=5432 -DB_USER=admin -DB_PASS=admin - -# Declare script helper -TEXT_HELPER="\nThis script aims to perform prisma migrations. - -It is needed to export shell variables 'DB_USER', 'DB_PASS', 'DB_PORT' and 'DB_NAME'. Default are : - - DB_USER: $DB_USER - DB_PASS: $DB_PASS - DB_NAME: $DB_NAME - -Following flags are available: - - -r Reset the database. Default is "$RESET_DB". - - -h Print script help.\n\n" - -print_help() { - printf "$TEXT_HELPER" -} - -# Parse options -while getopts hr flag -do - case "${flag}" in - r) - RESET_DB=true;; - h | *) - print_help - exit 0;; - esac -done - - -# Override database variables for local access -export DB_URL="postgresql://$DB_USER:$DB_PASS@localhost:$DB_PORT/$DB_NAME?schema=public" - -# Start database container -printf "\n${red}[db wrapper]${no_color}: Start postgres container\n" -docker run \ - --name postgres-migration \ - --publish $DB_PORT:$DB_PORT \ - --env POSTGRES_USER=$DB_USER \ - --env POSTGRES_PASSWORD=$DB_PASS \ - --env POSTGRES_DB=$DB_NAME \ - --detach \ - postgres - -sleep 3 - -# Start prisma migration -if [ "$RESET_DB" = "true" ]; then - printf "\n${red}[db wrapper]${no_color}: Start prisma reset\n" - pnpm --filter @cpn-console/server run db:reset -fi -printf "\n${red}[db wrapper]${no_color}: Start prisma migration\n" -pnpm --filter @cpn-console/server run db:migrate - -# Stop database container -printf "\n${red}[db wrapper]${no_color}: Stop and remove postgres container\n" -docker stop postgres-migration -docker rm postgres-migration diff --git a/apps/server-nestjs/src/cpin-module/old-server/nodemon.json b/apps/server-nestjs/src/cpin-module/old-server/nodemon.json deleted file mode 100644 index a83d0540d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/nodemon.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "watch": ["server.ts", "src/"], - "ext": "js, ts" -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts b/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts deleted file mode 100644 index 057121c97..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/prisma.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import path from 'node:path' -import { defineConfig } from 'prisma/config' - -export default defineConfig({ - schema: path.join('src', 'prisma', 'schema'), - migrations: { - path: path.join('src', 'prisma', 'migrations'), - }, -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts deleted file mode 100644 index 2a871fd47..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/prisma.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import { beforeEach, vi } from 'vitest' -import { mockDeep, mockReset } from 'vitest-mock-extended' - -vi.mock('../prisma') - -const prisma = mockDeep() - -beforeEach(() => { - // reset les mocks - mockReset(prisma) -}) - -export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts deleted file mode 100644 index 50939fd6f..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/__mocks__/utils/hook-wrapper.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { beforeEach, vi } from 'vitest' -import { mockDeep, mockReset } from 'vitest-mock-extended' - -vi.mock('../utils/hook-wrapper') - -export const hook = { - cluster: { - delete: vi.fn(), - upsert: vi.fn(), - }, - misc: { - checkServices: vi.fn(), - syncRepository: vi.fn(), - }, - project: { - upsert: vi.fn(), - delete: vi.fn(), - getSecrets: vi.fn(), - }, - user: { - retrieveUserByEmail: vi.fn(), - }, - zone: { - delete: vi.fn(), - upsert: vi.fn(), - }, -} as const - -const hookMock = mockDeep() - -beforeEach(() => { - // reset les mocks - mockReset(hookMock) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts deleted file mode 100644 index e71dad6c1..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -import { apiPrefix } from '@cpn-console/shared' -import app from './app' -import { getRandomRequestor, setRequestor } from './utils/mocks' - -vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks')).mockSessionPlugin) - -describe('app', () => { - beforeEach(() => { - setRequestor(getRandomRequestor()) - }) - afterAll(async () => { - await app.close() - }) - - it('should respond 404 on unknown route', async () => { - const response = await app.inject() - .get(`${apiPrefix}/miss`) - expect(response.statusCode).toBe(404) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/app.ts deleted file mode 100644 index 1c4c2d09d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/app.ts +++ /dev/null @@ -1,59 +0,0 @@ -// import type { FastifyRequest } from 'fastify' -// import fastify from 'fastify' -// import helmet from '@fastify/helmet' -// import keycloak from 'fastify-keycloak-adapter' -// import fastifySession from '@fastify/session' -// import fastifyCookie from '@fastify/cookie' -// import fastifySwagger from '@fastify/swagger' -// import fastifySwaggerUi from '@fastify/swagger-ui' -// import { initServer } from '@ts-rest/fastify' -// import { generateOpenApi } from '@ts-rest/open-api' -// import { apiPrefix, getContract } from '@cpn-console/shared' -// import { isDev, isInt, isTest } from './utils/env' -// import { fastifyConf, swaggerConf, swaggerUiConf } from './utils/fastify' -// import { apiRouter } from './resources/index' -// import { keycloakConf, sessionConf } from './utils/keycloak' -// import type { CustomLogger } from './utils/logger' -// import { log } from './utils/logger' - -// export const serverInstance: ReturnType = initServer() - -// const openApiDocument = generateOpenApi(await getContract(), swaggerConf, { setOperationId: true }) - -// const app = fastify(fastifyConf) - // .register(helmet, () => ({ - // contentSecurityPolicy: !(isInt || isDev || isTest), - // })) - // .register(fastifyCookie) - // .register(fastifySession, sessionConf) - // // @ts-ignore - // .register(keycloak, keycloakConf) - // .register(fastifySwagger, { transformObject: () => openApiDocument }) - // .register(fastifySwaggerUi, swaggerUiConf) - // .register(apiRouter()) - // .addHook('onRoute', (opts) => { - // if (opts.path === `${apiPrefix}/healthz`) { - // opts.logLevel = 'silent' - // } - // }) - // .setErrorHandler((error: Error, req: FastifyRequest, reply) => { - // const statusCode = 500 - // // @ts-ignore vérifier l'objet - // const message = error.description || error.message - // reply.status(statusCode).send({ status: statusCode, error: message, stack: error.stack }) - // log('info', { reqId: req.id, error }) - // }) - // .addHook('onResponse', (req, res) => { - // if (res.statusCode < 400) { - // req.log.info({ status: res.statusCode, userId: req.session?.user?.id }) - // } else if (res.statusCode < 500) { - // req.log.warn({ status: res.statusCode, userId: req.session?.user?.id }) - // } else { - // req.log.error({ status: res.statusCode, userId: req.session?.user?.id }) - // } - // }) - -// await app.ready() - -// export const logger = app.log as CustomLogger -// export default app diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts deleted file mode 100644 index c86eaec64..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { PrismaClientInitializationError } from '@prisma/client/runtime/library' -import prisma from './__mocks__/prisma' -import app, { logger } from './app' -import { getConnection } from './connect' - -vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks')).mockSessionPlugin) -vi.mock('@old-server/resources/queries-index') -vi.mock('./models/log', () => getModel('getLogModel')) -vi.mock('./models/repository', () => getModel('getRepositoryModel')) -vi.mock('./models/permission', () => getModel('getPermissionModel')) -vi.mock('./models/environment', () => getModel('getEnvironmentModel')) -vi.mock('./models/project', () => getModel('getProjectModel')) -vi.mock('./models/user', () => getModel('getUserModel')) -vi.mock('./models/users-projects', () => getModel('getRolesModel')) -vi.mock('./models/zone', () => getModel('getZoneModel')) -vi.mock('./prisma') - -vi.spyOn(app, 'listen') -vi.spyOn(logger, 'info') -vi.spyOn(logger, 'warn') -vi.spyOn(logger, 'error') -vi.spyOn(logger, 'debug') - -function getModel(modelName) { - return { - [modelName]: vi.fn(() => ({ - sync: vi.fn(), - hasMany: vi.fn(), - belongsTo: vi.fn(), - belongsToMany: vi.fn(), - })), - } -} - -describe('connect', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should connect to postgres', async () => { - await getConnection() - - expect(logger.info.mock.calls).toHaveLength(2) - expect(logger.info.mock.calls).toContainEqual([`Trying to connect to Postgres with: ${process.env.DB_URL}`]) - expect(logger.info.mock.calls).toContainEqual(['Connected to Postgres!']) - }) - - it('should fail to connect once, then connect to postgres', async () => { - const errorToCatch = new PrismaClientInitializationError('Failed to connect', '2.19.0', 'P1001') - - prisma.$connect.mockRejectedValueOnce(errorToCatch) - await getConnection() - - expect(logger.info.mock.calls).toHaveLength(5) - expect(logger.info.mock.calls).toContainEqual([`Trying to connect to Postgres with: ${process.env.DB_URL}`]) - expect(logger.info.mock.calls).toContainEqual(['Could not connect to Postgres: Failed to connect']) - expect(logger.info.mock.calls).toContainEqual(['Retrying (4 tries left)']) - expect(logger.info.mock.calls).toContainEqual(['Connected to Postgres!']) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts b/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts deleted file mode 100644 index dd673184d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/connect.ts +++ /dev/null @@ -1,52 +0,0 @@ -// import { setTimeout } from 'node:timers/promises' -// import prisma from './prisma' -// import { logger } from './app' -// import { - // dbUrl, - // isCI, - // isDev, - // isTest, -// } from './utils/env' - -// const DELAY_BEFORE_RETRY = isTest || isCI ? 1000 : 10000 -// let closingConnections = false - -// export async function getConnection(triesLeft = 5): Promise { - // if (closingConnections || triesLeft <= 0) { - // throw new Error('Unable to connect to Postgres server') - // } - // triesLeft-- - - // try { - // if (isDev || isTest || isCI) { - // logger.info(`Trying to connect to Postgres with: ${dbUrl}`) - // } - // await prisma.$connect() - - // logger.info('Connected to Postgres!') - // } catch (error) { - // if (triesLeft > 0) { - // logger.error(error) - // logger.info(`Could not connect to Postgres: ${error.message}`) - // logger.info(`Retrying (${triesLeft} tries left)`) - // await setTimeout(DELAY_BEFORE_RETRY) - // return getConnection(triesLeft) - // } - - // logger.info(`Could not connect to Postgres: ${error.message}`) - // logger.info('Out of retries') - // error.message = `Out of retries, last error: ${error.message}` - // throw error - // } -// } - -// export async function closeConnections() { - // closingConnections = true - // try { - // await prisma.$disconnect() - // } catch (error) { - // logger.error(error) - // } finally { - // closingConnections = false - // } -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts deleted file mode 100644 index c12f4f9d0..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/dump.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck - -/** - * How to use ? - * npx vite-node src/init/db/dump.ts - * format ./data.ts with linter - * cut/paste to packages/test-utils/src/imports/data.ts - */ - -import { writeFileSync } from 'node:fs' -import { Prisma } from '@prisma/client' -import { associations, manyToManyRelation, modelKeys, models, resourceListToDict } from './utils' -import prisma from '@old-server/prisma' - -const Models = resourceListToDict(Prisma.dmmf.datamodel.models) - -for (const modelKey of modelKeys) { - const modelDatas = await prisma[modelKey].findMany() - models[modelKey] = modelDatas -} -for (const [model, targetModel, relationKey] of manyToManyRelation) { - const modelKey = model.slice(0, 1).toLocaleLowerCase() + model.slice(1) - const modelDatas = await prisma[modelKey].findMany({ select: { [Models[model].id]: true, [relationKey]: { select: { [Models[targetModel].id]: true } } } }) - associations.push([modelKey, modelDatas]) -} -const a = JSON.stringify({ ...models, associations }, null, 2) - -writeFileSync('./data', `export const data = ${a}`) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts deleted file mode 100644 index cc1d8f04b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -// import { modelKeys } from './utils' -// import { logger } from '@old-server/app' -// import prisma from '@old-server/prisma' - -// type ExtractKeysWithFields = { - // [K in keyof T]: T[K] extends { fields: any } ? K : never -// }[keyof T] - -// type Models = ExtractKeysWithFields - -// type Imports = Partial> & { - // associations: [Models, any[]] -// } - -// export async function initDb(data: Imports) { - // const dataStringified = JSON.stringify(data) - // const dataParsed = JSON.parse(dataStringified, (key, value) => { - // try { - // if (['permissions', 'everyonePerms'].includes(key)) { - // return BigInt(value.slice(0, value.length - 1)) - // } - // } catch (_error) { - // return value - // } - // return value - // }) - // logger.info('Drop tables') - // for (const modelKey of modelKeys.toReversed()) { - // // @ts-ignore - // await prisma[modelKey].deleteMany() - // } - // logger.info('Import models') - // for (const modelKey of modelKeys) { - // // @ts-ignore - // await prisma[modelKey].createMany({ data: dataParsed[modelKey] }) - // } - // logger.info('Import associations') - // for (const [modelKey, rows] of dataParsed.associations) { - // for (const row of rows) { - // const idKey = 'id' - // const connectKeys = Object.keys(row).filter(key => key !== idKey) - // const dataConnects = connectKeys.reduce((acc, curr) => { - // acc[curr] = { connect: row[curr] } - // return acc - // }, {} as Record) - // // @ts-ignore - // await prisma[modelKey].update({ where: { id: row.id }, data: dataConnects }) - // } - // } - // logger.info('End import') -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts deleted file mode 100644 index 4377e07a1..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import prisma from '../../__mocks__/prisma' -import { modelKeys, moveBefore, resourceListToDict } from './utils' - -vi.mock('fs', () => ({ writeFileSync: vi.fn() })) -for (const modelKey of modelKeys) { - prisma[modelKey].findMany.mockResolvedValue([]) -} - -describe('test moveBefore', () => { - it('should be moved', () => { - const arr = ['a', 'b', 'c'] - const arrSorted = moveBefore(arr, 'c', 'b') - expect(arrSorted).toEqual(['a', 'c', 'b']) - - const arrSorted2 = moveBefore(arr, 'c', 'a') - expect(arrSorted2).toEqual(['c', 'a', 'b']) - }) - it('should not be moved', () => { - const arr = ['a', 'b', 'c'] - const arrSorted = moveBefore(arr, 'b', 'c') - expect(arrSorted).toEqual(false) - - const arrSorted2 = moveBefore(arr, 'a', 'c') - expect(arrSorted2).toEqual(false) - - const arrSorted3 = moveBefore(arr, 'c', 'c') - expect(arrSorted3).toEqual(false) - }) -}) - -it('test resourceListToDict (by name)', () => { - const list = [ - { name: 'a', value: 1 }, - { name: 'b', value: 2 }, - { name: 'c', value: 3 }, - ] - const dict = resourceListToDict(list) - expect(dict).toEqual({ - a: { name: 'a', value: 1 }, - b: { name: 'b', value: 2 }, - c: { name: 'c', value: 3 }, - }) -}) - -it('stringify bigint', () => { - const list = { name: 'a', value: 1n } - - const dict = JSON.stringify(list) - - expect(dict).toEqual('{"name":"a","value":"1n"}') -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts deleted file mode 100644 index 95924751a..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/init/db/utils.ts +++ /dev/null @@ -1,85 +0,0 @@ -// @ts-nocheck -import { Prisma } from '@prisma/client' - -// eslint-disable-next-line no-extend-native -BigInt.prototype.toJSON = function () { - return `${this.toString()}n` -} - -export type ResourceByName = Record -export function resourceListToDict(resList: Array): ResourceByName { - return resList.reduce((acc, curr) => { - return { - ...acc, - [curr.name]: curr, - } - }, {} as ResourceByName) -} - -// @ts-ignore -const Models = resourceListToDict(Prisma.dmmf.datamodel.models) -let ModelsNames = Object.keys(Models) -let ModelsOrder = [...ModelsNames] - -export function moveBefore(arr: T, toMove: T[number], ref: T[number]): T | false { - const iref = arr.indexOf(ref) - const moveref = arr.indexOf(toMove) - if (moveref <= iref) return false - return [ - ...arr.slice(0, iref), - arr[moveref], - ...arr.slice(iref, moveref), - ...arr.slice(moveref + 1), - ] as T -} - -export const manyToManyRelation: [string, string, string][] = [] -function sort() { - let hasChanged = false - for (const model of ModelsNames) { - for (const field of Models[model].fields) { - if (field.isId) Models[model].id = field.name - if (field.type in Models) { - const relationField = Models[field.type].fields.find(({ type }) => type === model) - if (!relationField) throw new Error('unable to find matching model') - if ( - (relationField.isRequired && field.isRequired && !relationField.isList) - || (relationField.isRequired && !field.isRequired) - ) { - const moveRes = moveBefore(ModelsOrder, model, field.type) - if (moveRes) { - hasChanged = true - ModelsOrder = moveRes - } - } - if ( - field.isList && relationField.isList - && !manyToManyRelation.find(test => - (test[0] === model && test[1] === field.type) || (test[0] === field.type && test[1] === model)) - ) { - manyToManyRelation.push([model, field.type, field.name]) - } - } - } - } - ModelsNames = ModelsOrder - if (hasChanged) sort() -} - -sort() - -// special case to study -const logUserCase = moveBefore(ModelsOrder, 'User', 'Log') -if (logUserCase) { - ModelsOrder = logUserCase -} -const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log') -if (logProjectCase) { - ModelsOrder = logProjectCase -} - -export const models: Record = {} -export const associations: Record = [] -export const modelKeys = ModelsOrder.map(model => model.slice(0, 1).toLocaleLowerCase() + model.slice(1)) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts deleted file mode 100644 index 2a871fd47..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/prisma.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { PrismaClient } from '@prisma/client' -import { beforeEach, vi } from 'vitest' -import { mockDeep, mockReset } from 'vitest-mock-extended' - -vi.mock('../prisma') - -const prisma = mockDeep() - -beforeEach(() => { - // reset les mocks - mockReset(prisma) -}) - -export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts deleted file mode 100644 index 3e9556625..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/mocks/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import fp from 'fastify-plugin' -import type { User } from '@cpn-console/test-utils' - -let requestor: User - -export function setRequestor(user: User) { - requestor = user -} - -export function getRequestor() { - return requestor -} - -export async function mockSessionPlugin() { - const sessionPlugin = (app, opt, next) => { - app.addHook('onRequest', (req, res, next) => { - req.session = { user: getRequestor() } - next() - }) - next() - } - - return { default: fp(sessionPlugin) } -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts deleted file mode 100644 index d18cfcf39..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/plugins.ts +++ /dev/null @@ -1,46 +0,0 @@ -// import { readdirSync, statSync } from 'node:fs' -// import { type Plugin, pluginManager } from '@cpn-console/hooks' -// import { plugin as argo } from '@cpn-console/argocd-plugin' -// import { plugin as gitlab } from '@cpn-console/gitlab-plugin' -// import { plugin as harbor } from '@cpn-console/harbor-plugin' -// import { plugin as keycloak } from '@cpn-console/keycloak-plugin' -// import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin' -// import { plugin as nexus } from '@cpn-console/nexus-plugin' -// import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin' -// import { plugin as vault } from '@cpn-console/vault-plugin' -// import { pluginManagerOptions } from './utils/plugins' -// import { pluginsDir } from './utils/env' - -// export async function initPm() { - // const pm = pluginManager(pluginManagerOptions) - // pm.register(argo) - // pm.register(gitlab) - // pm.register(harbor) - // pm.register(keycloak) - // pm.register(kubernetes) - // pm.register(nexus) - // pm.register(sonarqube) - // pm.register(vault) - - // if (!statSync(pluginsDir, { - // throwIfNoEntry: false, - // })) { - // return pm - // } - // for (const dirName of readdirSync(pluginsDir)) { - // const moduleAbsPath = `${pluginsDir}/${dirName}` - // try { - // statSync(`${moduleAbsPath}/package.json`) - // const pkg = await import(`${moduleAbsPath}/package.json`, { with: { type: 'json' } }) - // const entrypoint = pkg.default.module || pkg.default.main - // if (!entrypoint) throw new Error(`No entrypoint found in package.json : ${pkg.default.name}`) - // const { plugin } = await import(`${moduleAbsPath}/${entrypoint}`) as { plugin: Plugin } - // pm.register(plugin) - // } catch (error) { - // console.error(`Could not import module ${moduleAbsPath}`) - // console.error(error.stack) - // } - // } - - // return pm -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts deleted file mode 100644 index 2e8f0cb4b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { getPreparedApp } from './prepare-app' -import { getConnection } from './connect' -import { initDb } from './init/db/index' -import app, { logger } from './app' - -vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks')).mockSessionPlugin) -vi.mock('./connect') -vi.mock('./index') -vi.mock('./utils/logger') -vi.mock('./init/db/index', () => ({ initDb: vi.fn() })) - -vi.spyOn(app, 'listen') -vi.spyOn(logger, 'info') -vi.spyOn(logger, 'warn') -vi.spyOn(logger, 'error') -vi.spyOn(logger, 'debug') - -describe('server', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should getConnection', async () => { - // const port = Math.round(Math.random() * 10000) + 1024 - await getPreparedApp().catch(err => console.warn(err)) - - expect(getConnection).toHaveBeenCalledTimes(1) - expect(initDb.mock.calls).toHaveLength(1) - }) - - it('should throw an error on connection to DB', async () => { - const error = new Error('This is OK!') - getConnection.mockRejectedValueOnce(error) - - let response - await getPreparedApp() - .catch((err) => { response = err }) - - expect(getConnection.mock.calls).toHaveLength(1) - expect(app.listen.mock.calls).toHaveLength(0) - expect(response).toMatchObject(error) - }) - - it('should throw an error on initDb import if module is not found', async () => { - const error = new Error('Failed to load') - initDb.mockRejectedValueOnce(error) - - await getPreparedApp() - - expect(initDb.mock.calls).toHaveLength(1) - expect(logger.info.mock.calls).toHaveLength(3) - }) - - it('should throw an error on initDb import', async () => { - const error = new Error('This is OK!') - initDb.mockRejectedValueOnce(error) - - let response - try { - await getPreparedApp() - } catch (err) { - response = err - } - - expect(initDb.mock.calls).toHaveLength(1) - expect(logger.info.mock.calls).toHaveLength(2) - expect(response).toMatchObject(error) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts deleted file mode 100644 index b2ab294f9..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prepare-app.ts +++ /dev/null @@ -1,106 +0,0 @@ -// import { rm } from 'node:fs/promises' -// import { dirname, resolve } from 'node:path' -// import { fileURLToPath } from 'node:url' -// import { isCI, isDev, isDevSetup, isInt, isProd, isTest, port } from './utils/env' -// import app, { logger } from './app' -// import { getConnection } from './connect' -// import { initDb } from './init/db/index' -// import { initPm } from './plugins' - -// // Workaround because fetch isn't using http_proxy variables -// // See. https://github.com/gajus/global-agent/issues/52#issuecomment-1134525621 -// if (process.env.HTTP_PROXY) { - // const Undici = await import('undici') - // const ProxyAgent = Undici.ProxyAgent - // const setGlobalDispatcher = Undici.setGlobalDispatcher - // setGlobalDispatcher( - // new ProxyAgent(process.env.HTTP_PROXY), - // ) -// } - -// async function initializeDB(path: string) { - // logger.info('Starting init DB...') - // const { data } = await import(path) - // await initDb(data) - // logger.info('initDb invoked successfully') -// } - -// export async function startServer(defaultPort: number = (port ? +port : 8080)) { - // try { - // await getConnection() - // } catch (error) { - // if (!(error instanceof Error)) return - // logger.error(error.message) - // throw error - // } - - // initPm() - - // logger.info('Reading init database file') - - // try { - // const dataPath = (isProd || isInt) - // ? './init/db/imports/data' - // : '@cpn-console/test-utils/src/imports/data' - // await initializeDB(dataPath) - // if (isProd && !isDevSetup) { - // logger.info('Cleaning up imported data file...') - // const __filename = fileURLToPath(import.meta.url) - // const __dirname = dirname(__filename) - // await rm(resolve(__dirname, dataPath)) - // logger.info(`Successfully deleted '${dataPath}'`) - // } - // } catch (error) { - // if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { - // logger.info('No initDb file, skipping') - // } else { - // logger.warn(error.message) - // throw error - // } - // } - - // try { - // await app.listen({ host: '0.0.0.0', port: defaultPort ?? 8080 }) - // } catch (error) { - // logger.error(error) - // process.exit(1) - // } - // logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) -// } - -// export async function getPreparedApp() { - // try { - // await getConnection() - // } catch (error) { - // logger.error(error.message) - // throw error - // } - - // initPm() - - // logger.info('Reading init database file') - - // try { - // const dataPath = (isProd || isInt) - // ? './init/db/imports/data' - // : '@cpn-console/test-utils/src/imports/data' - // await initializeDB(dataPath) - // if (isProd && !isDevSetup) { - // logger.info('Cleaning up imported data file...') - // const __filename = fileURLToPath(import.meta.url) - // const __dirname = dirname(__filename) - // await rm(resolve(__dirname, dataPath)) - // logger.info(`Successfully deleted '${dataPath}'`) - // } - // } catch (error) { - // if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message.includes('Failed to load') || error.message.includes('Cannot find module')) { - // logger.info('No initDb file, skipping') - // } else { - // logger.warn(error.message) - // throw error - // } - // } - - // logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) - // return app -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts b/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts deleted file mode 100644 index 4590932b6..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from '@prisma/client' - -const prisma = new PrismaClient() - -export default prisma diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230706084346_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230706084346_dso/migration.sql deleted file mode 100644 index f2f4e7b0b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230706084346_dso/migration.sql +++ /dev/null @@ -1,151 +0,0 @@ --- CreateTable -CREATE TABLE "Environment" ( - "id" UUID NOT NULL, - "name" TEXT NOT NULL, - "projectId" UUID NOT NULL, - "status" TEXT NOT NULL DEFAULT 'initializing', - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Environment_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Log" ( - "id" UUID NOT NULL, - "data" JSONB NOT NULL, - "action" TEXT NOT NULL DEFAULT '', - "userId" UUID NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Log_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Organization" ( - "id" UUID NOT NULL, - "source" TEXT NOT NULL, - "name" TEXT NOT NULL, - "label" TEXT NOT NULL, - "active" BOOLEAN NOT NULL DEFAULT true, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Permission" ( - "id" UUID NOT NULL, - "userId" UUID NOT NULL, - "environmentId" UUID NOT NULL, - "level" INTEGER NOT NULL DEFAULT 0, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Permission_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Project" ( - "id" UUID NOT NULL, - "name" TEXT NOT NULL, - "organizationId" UUID NOT NULL, - "description" TEXT, - "status" TEXT NOT NULL, - "locked" BOOLEAN NOT NULL DEFAULT false, - "services" JSONB NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Project_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Repository" ( - "id" UUID NOT NULL, - "projectId" UUID NOT NULL, - "internalRepoName" TEXT NOT NULL, - "externalRepoUrl" TEXT NOT NULL, - "externalUserName" TEXT, - "externalToken" TEXT, - "isInfra" BOOLEAN NOT NULL DEFAULT false, - "isPrivate" BOOLEAN NOT NULL DEFAULT false, - "status" TEXT NOT NULL DEFAULT 'initializing', - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Repository_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "User" ( - "id" UUID NOT NULL, - "firstName" TEXT NOT NULL, - "lastName" TEXT NOT NULL, - "email" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Role" ( - "userId" UUID NOT NULL, - "projectId" UUID NOT NULL, - "role" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Role_pkey" PRIMARY KEY ("userId","projectId") -); - --- CreateIndex -CREATE UNIQUE INDEX "Organization_id_key" ON "Organization"("id"); - --- CreateIndex -CREATE UNIQUE INDEX "Organization_name_key" ON "Organization"("name"); - --- CreateIndex -CREATE UNIQUE INDEX "Organization_label_key" ON "Organization"("label"); - --- CreateIndex -CREATE UNIQUE INDEX "Permission_id_key" ON "Permission"("id"); - --- CreateIndex -CREATE UNIQUE INDEX "Permission_userId_environmentId_key" ON "Permission"("userId", "environmentId"); - --- CreateIndex -CREATE UNIQUE INDEX "Project_id_key" ON "Project"("id"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "Role_userId_projectId_key" ON "Role"("userId", "projectId"); - --- AddForeignKey -ALTER TABLE "Environment" ADD CONSTRAINT "Environment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Log" ADD CONSTRAINT "Log_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Permission" ADD CONSTRAINT "Permission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Permission" ADD CONSTRAINT "Permission_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Repository" ADD CONSTRAINT "Repository_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Role" ADD CONSTRAINT "Role_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230710181052_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230710181052_dso/migration.sql deleted file mode 100644 index 26e1ade3f..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230710181052_dso/migration.sql +++ /dev/null @@ -1,85 +0,0 @@ --- CreateEnum -CREATE TYPE "ClusterPrivacy" AS ENUM ('public', 'dedicated'); - --- CreateTable -CREATE TABLE "Cluster" ( - "id" UUID NOT NULL, - "label" VARCHAR(50) NOT NULL, - "privacy" "ClusterPrivacy" NOT NULL DEFAULT 'dedicated', - "secretName" VARCHAR(50) NOT NULL, - "clusterResources" BOOLEAN NOT NULL DEFAULT false, - "kubeConfigId" UUID NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Cluster_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Kubeconfig" ( - "id" UUID NOT NULL, - "user" JSONB NOT NULL, - "cluster" JSONB NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "parentClusterId" UUID, - - CONSTRAINT "Kubeconfig_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "_ClusterToEnvironment" ( - "A" UUID NOT NULL, - "B" UUID NOT NULL -); - --- CreateTable -CREATE TABLE "_ClusterToProject" ( - "A" UUID NOT NULL, - "B" UUID NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "Cluster_id_key" ON "Cluster"("id"); - --- CreateIndex -CREATE UNIQUE INDEX "Cluster_label_key" ON "Cluster"("label"); - --- CreateIndex -CREATE UNIQUE INDEX "Cluster_secretName_key" ON "Cluster"("secretName"); - --- CreateIndex -CREATE UNIQUE INDEX "Cluster_kubeConfigId_key" ON "Cluster"("kubeConfigId"); - --- CreateIndex -CREATE UNIQUE INDEX "Kubeconfig_id_key" ON "Kubeconfig"("id"); - --- CreateIndex -CREATE UNIQUE INDEX "Kubeconfig_parentClusterId_key" ON "Kubeconfig"("parentClusterId"); - --- CreateIndex -CREATE UNIQUE INDEX "_ClusterToEnvironment_AB_unique" ON "_ClusterToEnvironment"("A", "B"); - --- CreateIndex -CREATE INDEX "_ClusterToEnvironment_B_index" ON "_ClusterToEnvironment"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_ClusterToProject_AB_unique" ON "_ClusterToProject"("A", "B"); - --- CreateIndex -CREATE INDEX "_ClusterToProject_B_index" ON "_ClusterToProject"("B"); - --- AddForeignKey -ALTER TABLE "Cluster" ADD CONSTRAINT "Cluster_kubeConfigId_fkey" FOREIGN KEY ("kubeConfigId") REFERENCES "Kubeconfig"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_ClusterToEnvironment" ADD CONSTRAINT "_ClusterToEnvironment_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_ClusterToEnvironment" ADD CONSTRAINT "_ClusterToEnvironment_B_fkey" FOREIGN KEY ("B") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_B_fkey" FOREIGN KEY ("B") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230711132934_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230711132934_dso/migration.sql deleted file mode 100644 index 8f3fb5ff9..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230711132934_dso/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `parentClusterId` on the `Kubeconfig` table. All the data in the column will be lost. - -*/ --- DropIndex -DROP INDEX "Kubeconfig_parentClusterId_key"; - --- AlterTable -ALTER TABLE "Kubeconfig" DROP COLUMN "parentClusterId"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230802143822_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230802143822_dso/migration.sql deleted file mode 100644 index 4eb37edc5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230802143822_dso/migration.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TYPE "ProjectStatus" AS ENUM ('initializing', 'created', 'failed', 'archived'); - -ALTER TABLE public."Project" ALTER COLUMN status TYPE "ProjectStatus" USING - case - when status = 'created' then 'created'::"ProjectStatus" - when status = 'failed' then 'failed'::"ProjectStatus" - when status = 'archived' then 'archived'::"ProjectStatus" - else 'initializing'::"ProjectStatus" - end; -ALTER TABLE public."Project" ALTER COLUMN status SET DEFAULT 'initializing'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230912084459_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230912084459_dso/migration.sql deleted file mode 100644 index f402a0e3d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20230912084459_dso/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Cluster" ADD COLUMN "infos" VARCHAR(200); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231010111515_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231010111515_dso/migration.sql deleted file mode 100644 index f553e7880..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231010111515_dso/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `externalToken` on the `Repository` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Repository" DROP COLUMN "externalToken"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125838_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125838_dso/migration.sql deleted file mode 100644 index 394b650a5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125838_dso/migration.sql +++ /dev/null @@ -1,81 +0,0 @@ --- CreateEnum -CREATE TYPE "QuotaStageStatus" AS ENUM -('active', 'pendingDelete'); - --- Create new tables --- CreateTable -CREATE TABLE "Quota" -( - "id" UUID NOT NULL, - "memory" VARCHAR NOT NULL, - "cpu" REAL NOT NULL, - "name" VARCHAR NOT NULL, - "isPrivate" BOOLEAN NOT NULL DEFAULT false, - - CONSTRAINT "Quota_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Stage" -( - "id" UUID NOT NULL, - "name" VARCHAR NOT NULL, - - CONSTRAINT "Stage_pkey" PRIMARY KEY ("id") -); - --- Associate Quotas and Stages --- CreateTable -CREATE TABLE "QuotaStage" -( - "id" UUID NOT NULL, - "quotaId" UUID NOT NULL, - "stageId" UUID NOT NULL, - "status" "QuotaStageStatus" NOT NULL DEFAULT 'active', - - CONSTRAINT "QuotaStage_pkey" PRIMARY KEY ("id") -); -CREATE UNIQUE INDEX "Quota_id_key" ON "Quota"("id"); -CREATE UNIQUE INDEX "Quota_name_key" ON "Quota"("name"); -CREATE UNIQUE INDEX "Stage_id_key" ON "Stage"("id"); -CREATE UNIQUE INDEX "Stage_name_key" ON "Stage"("name"); -CREATE UNIQUE INDEX "QuotaStage_id_key" ON "QuotaStage"("id"); -CREATE UNIQUE INDEX "QuotaStage_quotaId_stageId_key" ON "QuotaStage"("quotaId", "stageId"); -ALTER TABLE "QuotaStage" ADD CONSTRAINT "QuotaStage_quotaId_fkey" FOREIGN KEY ("quotaId") REFERENCES "Quota"("id") ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE "QuotaStage" ADD CONSTRAINT "QuotaStage_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- Create default values for Quotas and Stages --- Quota -INSERT INTO "Quota" - (id, cpu, memory, "name", "isPrivate") -VALUES - ('5a57b62f-2465-4fb6-a853-5a751d099199', 2, '4Gi', 'small', false), - ('08770663-3b76-4af6-8978-9f75eda4faa7', 4, '8Gi', 'medium', false), - ('b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', 6, '12Gi', 'large', false), - ('97b851e8-9067-4a3d-a0e8-c3a6820c49be', 8, '16Gi', 'xlarge', false); - --- Stage -INSERT INTO "Stage" - (id, "name") -VALUES - ('4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', 'dev'), - ('38fa869d-6267-441d-af7f-e0548fd06b7e', 'staging'), - ('d434310e-7850-4d59-b47f-0772edf50582', 'integration'), - ('9b3e9991-896d-4d90-bdc5-a34be8c06b8f', 'prod'); - --- QuotaStage -INSERT INTO "QuotaStage" - (id, "quotaId", "stageId") -VALUES - ('0cb0c549-560e-4f26-8f4e-832dd722f68a', '5a57b62f-2465-4fb6-a853-5a751d099199', '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), - ('0530e9c9-b37d-4dec-93e6-1895f700e61c', '5a57b62f-2465-4fb6-a853-5a751d099199', '38fa869d-6267-441d-af7f-e0548fd06b7e'), - ('8a99db49-b7b1-44bf-865d-5e709e8aa0fc', '5a57b62f-2465-4fb6-a853-5a751d099199', 'd434310e-7850-4d59-b47f-0772edf50582'), - ('67561f00-d219-4ca6-b94a-3ee83f09d2d6', '5a57b62f-2465-4fb6-a853-5a751d099199', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), - ('8b3c201e-7518-4254-a94a-16c404e46936', '08770663-3b76-4af6-8978-9f75eda4faa7', '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), - ('9157ae12-3e39-43f8-a24f-ae5d9c6b69b7', '08770663-3b76-4af6-8978-9f75eda4faa7', '38fa869d-6267-441d-af7f-e0548fd06b7e'), - ('c733a1dd-c9fd-4def-b29e-df49ef7b6698', '08770663-3b76-4af6-8978-9f75eda4faa7', 'd434310e-7850-4d59-b47f-0772edf50582'), - ('15a51f47-0ab2-4a94-a808-722639d8c092', '08770663-3b76-4af6-8978-9f75eda4faa7', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), - ('cb66e80c-2304-472d-bc19-a411011674ca', 'b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', 'd434310e-7850-4d59-b47f-0772edf50582'), - ('59fb0e79-3a76-4b96-81d4-63f4caa98cfa', 'b7b4d9bd-7a8f-4287-bb12-5ce2dadb4ff2', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'), - ('4174b22c-2bee-4f4a-9d85-da7b5463f214', '97b851e8-9067-4a3d-a0e8-c3a6820c49be', 'd434310e-7850-4d59-b47f-0772edf50582'), - ('de0589b6-7cf5-4f1e-ab44-53e71a6cdb7a', '97b851e8-9067-4a3d-a0e8-c3a6820c49be', '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125839_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125839_dso/migration.sql deleted file mode 100644 index 8c98a7f74..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125839_dso/migration.sql +++ /dev/null @@ -1,35 +0,0 @@ --- Multiplication des environnements par clusteurs -ALTER TABLE "Environment" ADD COLUMN "clusterId" UUID; - -DO -$$ -DECLARE - perm record; - cte record; - env_uuid UUID; -BEGIN - FOR cte IN SELECT "B" AS environmentId, "A" AS "clusterId", "name", "projectId", status, "updatedAt", "createdAt" - FROM public."_ClusterToEnvironment", public."Environment" - WHERE public."Environment".id = "B" - LOOP - env_uuid := gen_random_uuid(); - INSERT INTO public."Environment" (id, "name", "projectId", "clusterId", status, "createdAt", "updatedAt") VALUES - (env_uuid, cte."name", cte."projectId", cte."clusterId", cte.status, cte."createdAt", cte."updatedAt"); - - FOR perm in SELECT * FROM public."Permission" WHERE "environmentId" = cte.environmentId - LOOP - INSERT INTO public."Permission" (id, "level", "createdAt", "updatedAt", "environmentId", "userId") VALUES - (gen_random_uuid(), perm."level", perm."createdAt", perm."updatedAt", env_uuid, perm."userId"); - END LOOP; - END LOOP; -END; -$$ -; -DELETE FROM public."Environment" WHERE "clusterId" is null; -ALTER TABLE public."Environment" ALTER COLUMN "clusterId" SET NOT NULL; -ALTER TABLE "Environment" ADD CONSTRAINT "Environment_clusterId_fkey" FOREIGN KEY ("clusterId") REFERENCES "Cluster"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- Delete old _ClusterToEnvironment -ALTER TABLE "_ClusterToEnvironment" DROP CONSTRAINT "_ClusterToEnvironment_A_fkey"; -ALTER TABLE "_ClusterToEnvironment" DROP CONSTRAINT "_ClusterToEnvironment_B_fkey"; -DROP TABLE "_ClusterToEnvironment"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125841_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125841_dso/migration.sql deleted file mode 100644 index 035cd1c85..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231011125841_dso/migration.sql +++ /dev/null @@ -1,36 +0,0 @@ --- Associate cluster to Stages --- CreateTable -CREATE TABLE "_ClusterToStage" ( - "A" UUID NOT NULL, - "B" UUID NOT NULL -); --- AddForeignKey -ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_A_fkey" FOREIGN KEY ("A") REFERENCES "Cluster"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_B_fkey" FOREIGN KEY ("B") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- CreateIndex -CREATE UNIQUE INDEX "_ClusterToStage_AB_unique" ON "_ClusterToStage"("A", "B"); - --- CreateIndex -CREATE INDEX "_ClusterToStage_B_index" ON "_ClusterToStage"("B"); - -DO -$$ -DECLARE - cluster record; - cte record; - env_uuid UUID; -BEGIN - FOR cluster IN SELECT id - FROM public."Cluster" - LOOP - INSERT INTO public."_ClusterToStage" ("A", "B") VALUES - (cluster.id, '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9'), - (cluster.id, '38fa869d-6267-441d-af7f-e0548fd06b7e'), - (cluster.id, 'd434310e-7850-4d59-b47f-0772edf50582'), - (cluster.id, '9b3e9991-896d-4d90-bdc5-a34be8c06b8f'); - END LOOP; -END; -$$ diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231012105520_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231012105520_dso/migration.sql deleted file mode 100644 index 43793bdb4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231012105520_dso/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ --- AlterTable -ALTER TABLE "Environment" ADD COLUMN "quotaStageId" UUID; -UPDATE "Environment" SET "quotaStageId" = '8b3c201e-7518-4254-a94a-16c404e46936' WHERE "name" = 'dev'; -UPDATE "Environment" SET "quotaStageId" = '9157ae12-3e39-43f8-a24f-ae5d9c6b69b7' WHERE "name" = 'staging'; -UPDATE "Environment" SET "quotaStageId" = '4174b22c-2bee-4f4a-9d85-da7b5463f214' WHERE "name" = 'integration'; -UPDATE "Environment" SET "quotaStageId" = 'de0589b6-7cf5-4f1e-ab44-53e71a6cdb7a' WHERE "name" = 'prod'; -ALTER TABLE "Environment" ALTER COLUMN "name" SET DATA TYPE VARCHAR(11); -ALTER TABLE "Environment" ALTER COLUMN "quotaStageId" SET NOT NULL; - --- AddForeignKey -ALTER TABLE "Environment" ADD CONSTRAINT "Environment_quotaStageId_fkey" FOREIGN KEY ("quotaStageId") REFERENCES "QuotaStage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231024155020_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231024155020_dso/migration.sql deleted file mode 100644 index 9af004b6a..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231024155020_dso/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Please read 6.0.0 Release notes ! --- lock all projects -UPDATE public."Project" SET "locked"=true \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231026150220_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231026150220_dso/migration.sql deleted file mode 100644 index d970d3965..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20231026150220_dso/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Please read 6.0.0 Release notes ! --- set all projects to failed to avoid unlock them -UPDATE public."Project" SET "status" = 'failed' WHERE "status" != 'archived' \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240112135751_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240112135751_dso/migration.sql deleted file mode 100644 index c387d9885..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240112135751_dso/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Log" ADD COLUMN "requestId" VARCHAR(21); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240321123436_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240321123436_dso/migration.sql deleted file mode 100644 index 18e20262c..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240321123436_dso/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `status` on the `Environment` table. All the data in the column will be lost. - - You are about to drop the column `status` on the `Repository` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Environment" DROP COLUMN "status"; - --- AlterTable -ALTER TABLE "Repository" DROP COLUMN "status"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240329172938_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240329172938_dso/migration.sql deleted file mode 100644 index f784b2156..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240329172938_dso/migration.sql +++ /dev/null @@ -1,46 +0,0 @@ --- AlterTable -ALTER TABLE "Cluster" ADD COLUMN "zoneId" UUID; - --- CreateTable -CREATE TABLE "Zone" -( - "id" UUID NOT NULL, - "slug" VARCHAR(10) NOT NULL, - "label" VARCHAR(50) NOT NULL, - "description" VARCHAR(200), - "createdAt" TIMESTAMP -(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP -(3) NOT NULL, - - CONSTRAINT "Zone_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Zone_id_key" ON "Zone"("id"); - --- CreateIndex -CREATE UNIQUE INDEX "Zone_slug_key" ON "Zone"("slug"); - --- Create default zone -INSERT INTO "Zone" - (id, "slug", "label", "description", "updatedAt") -VALUES - ('a66c4230-eba6-41f1-aae5-bb1e4f90cce0', 'default', 'Zone Défaut', 'Zone par défaut, à changer', CURRENT_TIMESTAMP); - --- Set default zoneId for current clusters -UPDATE "Cluster" -SET "zoneId" -= 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0' -WHERE "zoneId" -IS NULL; - --- AlterTable -ALTER TABLE "Cluster" ALTER COLUMN "zoneId" -SET -NOT NULL; - --- AddForeignKey -ALTER TABLE "Cluster" ADD CONSTRAINT "Cluster_zoneId_fkey" FOREIGN KEY ("zoneId") REFERENCES "Zone"("id") -ON DELETE RESTRICT ON -UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240424093852_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240424093852_dso/migration.sql deleted file mode 100644 index cb4f7ad96..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240424093852_dso/migration.sql +++ /dev/null @@ -1,23 +0,0 @@ --- CreateTable -CREATE TABLE "ProjectPlugin" ( - "pluginName" TEXT NOT NULL, - "projectId" UUID NOT NULL, - "key" TEXT NOT NULL, - "value" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "AdminPlugin" ( - "pluginName" TEXT NOT NULL, - "key" TEXT NOT NULL, - "value" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "ProjectPlugin_projectId_pluginName_key_key" ON "ProjectPlugin"("projectId", "pluginName", "key"); - --- CreateIndex -CREATE UNIQUE INDEX "AdminPlugin_pluginName_key_key" ON "AdminPlugin"("pluginName", "key"); - --- AddForeignKey -ALTER TABLE "ProjectPlugin" ADD CONSTRAINT "ProjectPlugin_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240427181037_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240427181037_dso/migration.sql deleted file mode 100644 index 11672324f..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240427181037_dso/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ -DO $$ -DECLARE - project_row RECORD; - registry_id INT; -BEGIN - -- Début de la boucle sur chaque ligne de la table 'Project' - FOR project_row IN SELECT id, services FROM public."Project" LOOP - -- Extrait 'registry.id' de la colonne JSON 'services' - registry_id := (SELECT (project_row.services -> 'registry' ->> 'id')::TEXT); - -- Si 'registry.id' existe, insérer dans la table 'config' - IF registry_id IS NOT NULL THEN - INSERT INTO public."ProjectPlugin" ("projectId", "pluginName", "key", "value") - VALUES (project_row.id, 'registry', 'projectId', registry_id::TEXT); - END IF; - END LOOP; -END $$; - --- AlterTable -ALTER TABLE "Project" DROP COLUMN "services"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240605135052_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240605135052_dso/migration.sql deleted file mode 100644 index 9c7d5a8f8..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240605135052_dso/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ --- CreateEnum -CREATE TYPE "RoleList" AS ENUM ('owner', 'user'); - --- AlterTable -ALTER TABLE public."Role" ALTER COLUMN "role" TYPE "RoleList" USING - case - when role = 'owner' then 'owner'::"RoleList" - else 'user'::"RoleList" - end; \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240612123132_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240612123132_dso/migration.sql deleted file mode 100644 index 45a4a5d1e..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240612123132_dso/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- CreateTable -CREATE TABLE "ProjectClusterHistory" ( - "projectId" UUID NOT NULL, - "clusterId" UUID NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "ProjectClusterHistory_projectId_clusterId_key" ON "ProjectClusterHistory"("projectId", "clusterId"); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240614222908_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240614222908_dso/migration.sql deleted file mode 100644 index 2b1641a65..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240614222908_dso/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -DO $$ -DECLARE - env_row RECORD; -BEGIN - -- Début de la boucle sur chaque ligne de la table 'Project' - FOR env_row IN SELECT "projectId", "clusterId" FROM public."Environment" LOOP - INSERT INTO public."ProjectClusterHistory" ("projectId", "clusterId") - VALUES (env_row."projectId", env_row."clusterId") - ON CONFLICT DO NOTHING; - END LOOP; -END $$; \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240618112205_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240618112205_dso/migration.sql deleted file mode 100644 index 5e7ff03d4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240618112205_dso/migration.sql +++ /dev/null @@ -1,58 +0,0 @@ --- AlterTable -ALTER TABLE "Environment" ADD COLUMN "quotaId" UUID, -ADD COLUMN "stageId" UUID; - --- AddForeignKey -ALTER TABLE "Environment" ADD CONSTRAINT "Environment_quotaId_fkey" FOREIGN KEY ("quotaId") REFERENCES "Quota"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Environment" ADD CONSTRAINT "Environment_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- CreateTable -CREATE TABLE "_QuotaToStage" ( - "A" UUID NOT NULL, - "B" UUID NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "_QuotaToStage_AB_unique" ON "_QuotaToStage"("A", "B"); - --- CreateIndex -CREATE INDEX "_QuotaToStage_B_index" ON "_QuotaToStage"("B"); - --- AddForeignKey -ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_A_fkey" FOREIGN KEY ("A") REFERENCES "Quota"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_B_fkey" FOREIGN KEY ("B") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -DO $$ -DECLARE - quota_stage_row RECORD; -BEGIN - FOR quota_stage_row IN SELECT * FROM public."QuotaStage" loop - UPDATE public."Environment" SET "stageId" = quota_stage_row."stageId" WHERE "Environment"."quotaStageId" = quota_stage_row.id; - UPDATE public."Environment" SET "quotaId" = quota_stage_row."quotaId" WHERE "Environment"."quotaStageId" = quota_stage_row.id; - insert into public."_QuotaToStage" values (quota_stage_row."quotaId", quota_stage_row."stageId"); - END LOOP; -END $$; - --- DropForeignKey -ALTER TABLE "Environment" DROP CONSTRAINT "Environment_quotaStageId_fkey"; - --- AlterTable -ALTER TABLE "Environment" ALTER COLUMN "quotaId" SET NOT NULL, -ALTER COLUMN "stageId" SET NOT NULL, -DROP COLUMN "quotaStageId"; - --- DropForeignKey -ALTER TABLE "QuotaStage" DROP CONSTRAINT "QuotaStage_quotaId_fkey"; - --- DropForeignKey -ALTER TABLE "QuotaStage" DROP CONSTRAINT "QuotaStage_stageId_fkey"; - --- DropTable -DROP TABLE "QuotaStage"; - --- DropEnum -DROP TYPE "QuotaStageStatus"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240717084709_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240717084709_dso/migration.sql deleted file mode 100644 index 0036da8a1..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240717084709_dso/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - Made the column `description` on table `Project` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -ALTER TABLE "Project" ALTER COLUMN "description" SET NOT NULL, -ALTER COLUMN "description" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240723135420_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240723135420_dso/migration.sql deleted file mode 100644 index ed6ae9b84..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240723135420_dso/migration.sql +++ /dev/null @@ -1,198 +0,0 @@ --- DropForeignKey if exists -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Permission_environmentId_fkey') THEN - ALTER TABLE "Permission" DROP CONSTRAINT "Permission_environmentId_fkey"; - END IF; -END $$; - --- DropForeignKey if exists -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Permission_userId_fkey') THEN - ALTER TABLE "Permission" DROP CONSTRAINT "Permission_userId_fkey"; - END IF; -END $$; - --- DropForeignKey if exists -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Role_projectId_fkey') THEN - ALTER TABLE "Role" DROP CONSTRAINT "Role_projectId_fkey"; - END IF; -END $$; - --- DropForeignKey if exists -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Role_userId_fkey') THEN - ALTER TABLE "Role" DROP CONSTRAINT "Role_userId_fkey"; - END IF; -END $$; - --- CreateTable if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ProjectMembers') THEN - CREATE TABLE "ProjectMembers" ( - "projectId" UUID NOT NULL, - "userId" UUID NOT NULL, - "roleIds" TEXT[] - ); - END IF; -END $$; - --- AlterTable -ALTER TABLE "Log" ADD COLUMN IF NOT EXISTS "projectId" UUID; - -INSERT INTO public."User" (id, "firstName", "lastName", email, "createdAt", "updatedAt") -VALUES('04ac168a-2c4f-4816-9cce-af6c612e5912'::uuid, 'Anonymous', 'User', 'anon@user', '2023-07-03 14:46:56.770', '2023-07-03 14:46:56.770') -ON CONFLICT (id) DO NOTHING; - --- AlterTable -ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "everyonePerms" BIGINT NOT NULL DEFAULT 896, -ADD COLUMN IF NOT EXISTS "ownerId" UUID; - -DO $$ -DECLARE - role_row RECORD; -BEGIN - IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Role') THEN - -- Début de la boucle sur chaque ligne de la table 'Project' - FOR role_row IN SELECT "userId", "projectId", "role" FROM public."Role" LOOP - INSERT INTO public."ProjectMembers" ("userId", "projectId", "roleIds") VALUES (role_row."userId", role_row."projectId", '{}'); - IF role_row."role" = 'owner'::public."RoleList" THEN - UPDATE public."Project" - SET "ownerId"=role_row."userId" - WHERE id=role_row."projectId"::uuid; - END IF; - END LOOP; - END IF; -END $$; - -UPDATE public."Project" -SET "ownerId"='04ac168a-2c4f-4816-9cce-af6c612e5912' -WHERE "ownerId" IS NULL; - -ALTER TABLE public."Project" ALTER COLUMN "ownerId" SET NOT NULL; - -DELETE FROM public."ProjectMembers" pm -USING public."Project" p -WHERE pm."userId" = p."ownerId" -AND pm."projectId" = p."id"; - --- DropTable if exists -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Permission') THEN - DROP TABLE "Permission"; - END IF; -END $$; - --- DropTable if exists -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'Role') THEN - DROP TABLE "Role"; - END IF; -END $$; - --- DropEnum if exists -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'RoleList') THEN - DROP TYPE "RoleList"; - END IF; -END $$; - --- CreateTable if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'AdminRole') THEN - CREATE TABLE "AdminRole" ( - "id" UUID NOT NULL, - "name" TEXT NOT NULL, - "permissions" BIGINT NOT NULL, - "position" SMALLINT NOT NULL, - CONSTRAINT "AdminRole_pkey" PRIMARY KEY ("id") - ); - END IF; -END $$; - --- CreateTable if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ProjectRole') THEN - CREATE TABLE "ProjectRole" ( - "id" UUID NOT NULL, - "name" TEXT NOT NULL, - "permissions" BIGINT NOT NULL, - "projectId" UUID NOT NULL, - "position" SMALLINT NOT NULL, - CONSTRAINT "ProjectRole_pkey" PRIMARY KEY ("id") - ); - END IF; -END $$; - --- CreateIndex if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'AdminRole_id_key') THEN - CREATE UNIQUE INDEX "AdminRole_id_key" ON "AdminRole"("id"); - END IF; -END $$; - --- CreateIndex if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'AdminRole_name_key') THEN - CREATE UNIQUE INDEX "AdminRole_name_key" ON "AdminRole"("name"); - END IF; -END $$; - --- CreateIndex if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'ProjectMembers_projectId_userId_key') THEN - CREATE UNIQUE INDEX "ProjectMembers_projectId_userId_key" ON "ProjectMembers"("projectId", "userId"); - END IF; -END $$; - --- CreateIndex if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'ProjectRole_id_key') THEN - CREATE UNIQUE INDEX "ProjectRole_id_key" ON "ProjectRole"("id"); - END IF; -END $$; - --- CreateIndex if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'Environment_projectId_name_key') THEN - CREATE UNIQUE INDEX "Environment_projectId_name_key" ON "Environment"("projectId", "name"); - END IF; -END $$; - --- AddForeignKey if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Log_projectId_fkey') THEN - ALTER TABLE "Log" ADD CONSTRAINT "Log_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; - END IF; -END $$; - --- AddForeignKey if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'Project_ownerId_fkey') THEN - ALTER TABLE "Project" ADD CONSTRAINT "Project_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; - --- AddForeignKey if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectMembers_projectId_fkey') THEN - ALTER TABLE "ProjectMembers" ADD CONSTRAINT "ProjectMembers_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; - --- AddForeignKey if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectMembers_userId_fkey') THEN - ALTER TABLE "ProjectMembers" ADD CONSTRAINT "ProjectMembers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; - --- AddForeignKey if not exists -DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'ProjectRole_projectId_fkey') THEN - ALTER TABLE "ProjectRole" ADD CONSTRAINT "ProjectRole_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; - --- AlterTable -ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "adminRoleIds" TEXT[]; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240725162050_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240725162050_dso/migration.sql deleted file mode 100644 index c9b41827b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240725162050_dso/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- DropIndex -DROP INDEX "AdminRole_name_key"; - --- AlterTable -ALTER TABLE "AdminRole" ADD COLUMN "oidcGroup" TEXT; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240726210139_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240726210139_dso/migration.sql deleted file mode 100644 index 265f262ab..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240726210139_dso/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ -/* - Warnings: - - - Made the column `oidcGroup` on table `AdminRole` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable - -UPDATE public."AdminRole" -SET "oidcGroup"='' -WHERE "oidcGroup" IS NULL; - -ALTER TABLE "AdminRole" ALTER COLUMN "oidcGroup" SET NOT NULL, -ALTER COLUMN "oidcGroup" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240808082632_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240808082632_dso/migration.sql deleted file mode 100644 index 4fc276860..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240808082632_dso/migration.sql +++ /dev/null @@ -1,17 +0,0 @@ --- CreateTable -CREATE TABLE "SystemSetting" -( - "key" TEXT NOT NULL, - "value" TEXT NOT NULL, - - CONSTRAINT "SystemSetting_pkey" PRIMARY KEY ("key") -); - --- CreateIndex -CREATE UNIQUE INDEX "SystemSetting_key_key" ON "SystemSetting"("key"); - --- Create maintenance setting -INSERT INTO "SystemSetting" - ("key", "value") -VALUES - ('maintenance', 'off'); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240826143230_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240826143230_dso/migration.sql deleted file mode 100644 index 95ab54869..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240826143230_dso/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ -INSERT INTO public."AdminRole" -(id, "name", permissions, "position", "oidcGroup") -VALUES('76229c96-4716-45bc-99da-00498ec9018c'::uuid, 'Admin', 2, 0, '/admin'); \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240829085548_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240829085548_dso/migration.sql deleted file mode 100644 index c11648218..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240829085548_dso/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - Warnings: - - - Made the column `externalUserName` on table `Repository` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -UPDATE "Repository" SET "externalUserName" = '' WHERE "externalUserName" IS NULL; - -ALTER TABLE "Repository" ALTER COLUMN "externalUserName" SET NOT NULL, -ALTER COLUMN "externalUserName" SET DEFAULT '', -ALTER COLUMN "externalRepoUrl" SET DEFAULT ''; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240916141253_token/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240916141253_token/migration.sql deleted file mode 100644 index b0472cd80..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240916141253_token/migration.sql +++ /dev/null @@ -1,23 +0,0 @@ --- CreateEnum -CREATE TYPE "TokenStatus" AS ENUM ('active', 'revoked'); - --- CreateTable -CREATE TABLE "AdminToken" ( - "id" UUID NOT NULL, - "name" TEXT NOT NULL, - "permissions" BIGINT NOT NULL, - "userId" UUID, - "expirationDate" TIMESTAMP(3), - "lastUse" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "status" "TokenStatus" NOT NULL DEFAULT 'active', - "hash" TEXT NOT NULL, - - CONSTRAINT "AdminToken_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "AdminToken_id_key" ON "AdminToken"("id"); - --- AddForeignKey -ALTER TABLE "AdminToken" ADD CONSTRAINT "AdminToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240919122331_optional_user_id/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240919122331_optional_user_id/migration.sql deleted file mode 100644 index 47488b00c..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240919122331_optional_user_id/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- DropForeignKey -ALTER TABLE "Log" DROP CONSTRAINT "Log_userId_fkey"; - --- AlterTable -ALTER TABLE "Log" ALTER COLUMN "userId" DROP NOT NULL; - --- AddForeignKey -ALTER TABLE "Log" ADD CONSTRAINT "Log_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923142722_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923142722_dso/migration.sql deleted file mode 100644 index 18eca3ead..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923142722_dso/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Log" ALTER COLUMN "requestId" SET DATA TYPE VARCHAR(36); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923155416_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923155416_dso/migration.sql deleted file mode 100644 index 74e0946f0..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240923155416_dso/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Zone" ADD COLUMN "argocdUrl" TEXT NOT NULL DEFAULT 'https://example.com'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240928002900_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240928002900_dso/migration.sql deleted file mode 100644 index 41dac7535..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20240928002900_dso/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterEnum -ALTER TYPE "ProjectStatus" ADD VALUE 'warning'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241008125724_enabling_maven/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241008125724_enabling_maven/migration.sql deleted file mode 100644 index ef888d5e5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241008125724_enabling_maven/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -DO $$ -DECLARE - project_row RECORD; - registry_id INT; -BEGIN - -- Début de la boucle sur chaque ligne de la table 'Project' - FOR project_row IN SELECT id FROM public."Project" WHERE status <> 'archived'::public."ProjectStatus" LOOP - INSERT INTO public."ProjectPlugin" ("projectId", "pluginName", "key", "value") - VALUES (project_row.id, 'nexus', 'activateMavenRepo', 'enabled') - ON CONFLICT DO NOTHING; - END LOOP; -END $$; \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232540_add_usertype/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232540_add_usertype/migration.sql deleted file mode 100644 index a57e5956c..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232540_add_usertype/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ --- CreateEnum -CREATE TYPE "UserType" AS ENUM ('human', 'bot', 'ghost'); - --- AlterEnum -ALTER TYPE "TokenStatus" ADD VALUE 'inactive'; - --- AlterTable -ALTER TABLE "User" ADD COLUMN "type" "UserType" NOT NULL DEFAULT 'human'; -UPDATE "User" SET type = 'ghost' WHERE id = '04ac168a-2c4f-4816-9cce-af6c612e5912'; - --- AlterTable -ALTER TABLE "User" ALTER COLUMN "type" DROP DEFAULT; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232541_add_pat/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232541_add_pat/migration.sql deleted file mode 100644 index 71e15a312..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241104232541_add_pat/migration.sql +++ /dev/null @@ -1,84 +0,0 @@ --- CreateTable (idempotent) -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'PersonalAccessToken') THEN - CREATE TABLE "PersonalAccessToken" ( - "id" UUID NOT NULL, - "name" TEXT NOT NULL, - "userId" UUID NOT NULL, - "expirationDate" TIMESTAMP(3) NOT NULL, - "lastUse" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "status" "TokenStatus" NOT NULL DEFAULT 'active', - "hash" TEXT NOT NULL, - - CONSTRAINT "PersonalAccessToken_pkey" PRIMARY KEY ("id") - ); - END IF; -END $$; - --- CreateIndex (idempotent) -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'PersonalAccessToken_id_key') THEN - CREATE UNIQUE INDEX "PersonalAccessToken_id_key" ON "PersonalAccessToken"("id"); - END IF; -END $$; - --- AddForeignKey (idempotent) -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'PersonalAccessToken_userId_fkey') THEN - ALTER TABLE "PersonalAccessToken" ADD CONSTRAINT "PersonalAccessToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - END IF; -END $$; - --- Process AdminToken (idempotent) -DO $$ -DECLARE - admin_token record; - user_uuid UUID; -BEGIN - FOR admin_token IN SELECT "name", "id" - FROM public."AdminToken" - LOOP - -- Generate new UUID if user does not exist - user_uuid := COALESCE( - (SELECT id FROM public."User" WHERE email = concat(admin_token.name, '@bot.id')), - gen_random_uuid() - ); - - -- Insert user if not already exists - INSERT INTO public."User" (id, "firstName", "lastName", email, "createdAt", "updatedAt", "type") - VALUES(user_uuid, 'Bot Admin', admin_token.name, concat(admin_token.name, '@bot.id'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'bot') - ON CONFLICT (id) DO NOTHING; - - -- Update AdminToken with the new user ID - UPDATE public."AdminToken" SET "userId" = user_uuid WHERE id = admin_token.id; - END LOOP; -END $$; - --- Alter AdminToken userId column to NOT NULL (idempotent) -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.columns - WHERE table_name = 'AdminToken' AND column_name = 'userId' AND is_nullable = 'NO') THEN - ALTER TABLE public."AdminToken" ALTER COLUMN "userId" SET NOT NULL; - END IF; -END $$; - --- DropForeignKey if exists (idempotent) -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'AdminToken_userId_fkey') THEN - ALTER TABLE "AdminToken" DROP CONSTRAINT "AdminToken_userId_fkey"; - END IF; -END $$; - --- AddForeignKey (idempotent) -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = 'AdminToken_userId_fkey') THEN - ALTER TABLE "AdminToken" ADD CONSTRAINT "AdminToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - END IF; -END $$; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241107142721_user_last_login/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241107142721_user_last_login/migration.sql deleted file mode 100644 index 521b2b10a..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241107142721_user_last_login/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "User" ADD COLUMN "lastLogin" TIMESTAMP(3); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112101945_add_slug/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112101945_add_slug/migration.sql deleted file mode 100644 index a7500833b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112101945_add_slug/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ --- AlterTable -ALTER TABLE "Project" ADD COLUMN "slug" TEXT; - -UPDATE public."Project" p -SET slug = ( - SELECT concat(org.name, '-', subp.name) FROM public."Project" subp - LEFT JOIN public."Organization" org on org."id" = subp."organizationId" - WHERE subp.id = p.id -); - -ALTER TABLE public."Project" ALTER COLUMN "slug" SET NOT NULL; - --- CreateIndex -CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug"); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql deleted file mode 100644 index b143cbeb9..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241112102015_add_provisionning_version/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Project" ADD COLUMN "lastSuccessProvisionningVersion" TEXT; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241216131342_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241216131342_dso/migration.sql deleted file mode 100644 index 7a8868190..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20241216131342_dso/migration.sql +++ /dev/null @@ -1,17 +0,0 @@ --- AlterTable -ALTER TABLE "_ClusterToProject" ADD CONSTRAINT "_ClusterToProject_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "_ClusterToProject_AB_unique"; - --- AlterTable -ALTER TABLE "_ClusterToStage" ADD CONSTRAINT "_ClusterToStage_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "_ClusterToStage_AB_unique"; - --- AlterTable -ALTER TABLE "_QuotaToStage" ADD CONSTRAINT "_QuotaToStage_AB_pkey" PRIMARY KEY ("A", "B"); - --- DropIndex -DROP INDEX "_QuotaToStage_AB_unique"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250107104749_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250107104749_dso/migration.sql deleted file mode 100644 index 21ce77b8d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250107104749_dso/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Cluster" ADD COLUMN "external" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql deleted file mode 100644 index ac63cd639..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222953_prevent_upgrade/migration.sql +++ /dev/null @@ -1,25 +0,0 @@ --- Vérifie les versions dans la table Project -DO $$ -DECLARE - project_id TEXT; - project_name TEXT; - last_version TEXT; -BEGIN - -- Boucle sur les projets non archivés - FOR project_id, project_name, last_version IN ( - SELECT id, name, "lastSuccessProvisionningVersion" - FROM "Project" - WHERE "status" != 'archived' - ) - LOOP - -- Vérifie si la version est NULL - IF last_version IS NULL THEN - RAISE EXCEPTION 'Le projet % (ID: %) a une version NULL.', project_name, project_id; - END IF; - - -- Vérifie si la version est inférieure à 8.23.0 selon SemVer - IF (string_to_array(last_version, '.')::int[] < ARRAY[8,23,0]) THEN - RAISE EXCEPTION 'Le projet % (ID: %) a une version (%), inférieure à 8.23.0.', project_name, project_id, last_version; - END IF; - END LOOP; -END $$; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222954_drop_organization/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222954_drop_organization/migration.sql deleted file mode 100644 index 54871c901..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250121222954_drop_organization/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `organizationId` on the `Project` table. All the data in the column will be lost. - - You are about to drop the `Organization` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "Project" DROP CONSTRAINT "Project_organizationId_fkey"; - --- AlterTable -ALTER TABLE "Project" DROP COLUMN "organizationId"; - --- DropTable -DROP TABLE "Organization"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250723141246_dso/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250723141246_dso/migration.sql deleted file mode 100644 index 68ca0df2f..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250723141246_dso/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Cluster" ALTER COLUMN "infos" SET DATA TYPE VARCHAR(1000); diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250818095032_remove_quota/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250818095032_remove_quota/migration.sql deleted file mode 100644 index 8364090d8..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250818095032_remove_quota/migration.sql +++ /dev/null @@ -1,44 +0,0 @@ --- AlterTable -ALTER TABLE "Environment" -ADD COLUMN "cpu" REAL NOT NULL DEFAULT 0, -ADD COLUMN "gpu" REAL NOT NULL DEFAULT 0, -ADD COLUMN "memory" REAL NOT NULL DEFAULT 0; - -COMMENT ON COLUMN "Environment".cpu IS 'CPU share as float (1 and 0.01 are valid values)'; -COMMENT ON COLUMN "Environment".gpu IS 'GPU share as float (1 and 0.01 are valid values)'; -COMMENT ON COLUMN "Environment".memory IS 'Memory value as GigaBytes (1 and 0.01 are valid values)'; - --- Use values from Quota. Memory is an extract of q.memory numeric value as it contains a unit (e.g. '2Gi'). -UPDATE "Environment" -SET cpu = q.cpu, memory = COALESCE(NULLIF(regexp_replace(q.memory, '\D', '','g'), ''), '0')::numeric -FROM "Quota" q -WHERE "quotaId" = q."id"; - -/* - Warnings: - - - You are about to drop the column `quotaId` on the `Environment` table. All the data in the column will be lost. - - You are about to drop the `Quota` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `_QuotaToStage` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "Environment" DROP CONSTRAINT "Environment_quotaId_fkey"; - --- DropForeignKey -ALTER TABLE "_QuotaToStage" DROP CONSTRAINT "_QuotaToStage_A_fkey"; - --- DropForeignKey -ALTER TABLE "_QuotaToStage" DROP CONSTRAINT "_QuotaToStage_B_fkey"; - --- AlterTable -ALTER TABLE "Environment" DROP COLUMN "quotaId", -ALTER COLUMN "cpu" DROP DEFAULT, -ALTER COLUMN "gpu" DROP DEFAULT, -ALTER COLUMN "memory" DROP DEFAULT; - --- DropTable -DROP TABLE "Quota"; - --- DropTable -DROP TABLE "_QuotaToStage"; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql deleted file mode 100644 index 77f32b5ab..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250825150622_add_cluster_resources/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- AlterTable -ALTER TABLE "Cluster" -ADD COLUMN "cpu" REAL NOT NULL DEFAULT 0, -ADD COLUMN "gpu" REAL NOT NULL DEFAULT 0, -ADD COLUMN "memory" REAL NOT NULL DEFAULT 0; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250916134454_add_project_resources/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250916134454_add_project_resources/migration.sql deleted file mode 100644 index decca804a..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20250916134454_add_project_resources/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ --- AlterTable -ALTER TABLE "Project" -ADD COLUMN "limitless" BOOLEAN NOT NULL DEFAULT true, -ADD COLUMN "hprodCpu" REAL NOT NULL DEFAULT 0, -ADD COLUMN "hprodGpu" REAL NOT NULL DEFAULT 0, -ADD COLUMN "hprodMemory" REAL NOT NULL DEFAULT 0, -ADD COLUMN "prodCpu" REAL NOT NULL DEFAULT 0, -ADD COLUMN "prodGpu" REAL NOT NULL DEFAULT 0, -ADD COLUMN "prodMemory" REAL NOT NULL DEFAULT 0; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql deleted file mode 100644 index 95f3a689d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251028150522_rename_default_zone/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Rename default zone -UPDATE "Zone" -SET ("label", "description") = ('DSO', 'Zone par défaut') -WHERE slug = 'default'; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql deleted file mode 100644 index aadb6cdba..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/20251208140951_add_argocd_inputs/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ --- AlterTable -ALTER TABLE "Repository" ADD COLUMN "deployRevision" TEXT, -ADD COLUMN "deployPath" TEXT, -ADD COLUMN "helmValuesFiles" TEXT; diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/migration_lock.toml b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/migration_lock.toml deleted file mode 100644 index 648c57fd5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" \ No newline at end of file diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/admin.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/admin.prisma deleted file mode 100644 index 71cfb1754..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/admin.prisma +++ /dev/null @@ -1,20 +0,0 @@ -model AdminPlugin { - pluginName String - key String - value String - - @@unique([pluginName, key]) -} - -model AdminRole { - id String @id @unique @default(uuid()) @db.Uuid - name String - permissions BigInt - position Int @db.SmallInt - oidcGroup String @default("") -} - -model SystemSetting { - key String @id @unique - value String -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma deleted file mode 100644 index e76048675..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/project.prisma +++ /dev/null @@ -1,106 +0,0 @@ -model Environment { - id String @id @default(uuid()) @db.Uuid - name String @db.VarChar(11) - projectId String @db.Uuid - memory Float @db.Real - cpu Float @db.Real - gpu Float @db.Real - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - clusterId String @db.Uuid - stageId String @db.Uuid - cluster Cluster @relation(fields: [clusterId], references: [id]) - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) - stage Stage @relation(fields: [stageId], references: [id]) - - @@unique([projectId, name]) -} - -model Repository { - id String @id @default(uuid()) @db.Uuid - projectId String @db.Uuid - internalRepoName String - externalRepoUrl String @default("") - externalUserName String @default("") - isInfra Boolean @default(false) - isPrivate Boolean @default(false) - deployRevision String @default("") - deployPath String @default("") - helmValuesFiles String @default("") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) -} - -model ProjectClusterHistory { - projectId String @db.Uuid - clusterId String @db.Uuid - - @@unique([projectId, clusterId]) -} - -model ProjectMembers { - projectId String @db.Uuid - userId String @db.Uuid - roleIds String[] - project Project @relation(fields: [projectId], references: [id]) - user User @relation(fields: [userId], references: [id]) - - @@unique([projectId, userId]) -} - -model ProjectPlugin { - pluginName String - projectId String @db.Uuid - key String - value String - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) - - @@unique([projectId, pluginName, key]) -} - -model ProjectRole { - id String @id @unique @default(uuid()) @db.Uuid - name String - permissions BigInt - projectId String @db.Uuid - position Int @db.SmallInt - project Project @relation(fields: [projectId], references: [id]) -} - -model Project { - id String @id @unique @default(uuid()) @db.Uuid - name String - description String @default("") - status ProjectStatus @default(initializing) - locked Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - everyonePerms BigInt @default(896) - ownerId String @db.Uuid - environments Environment[] - logs Log[] - owner User @relation(fields: [ownerId], references: [id]) - members ProjectMembers[] - plugins ProjectPlugin[] - roles ProjectRole[] - repositories Repository[] - clusters Cluster[] @relation("ClusterToProject") - slug String @unique - limitless Boolean @default(true) - hprodCpu Float @db.Real - hprodGpu Float @db.Real - hprodMemory Float @db.Real - prodCpu Float @db.Real - prodGpu Float @db.Real - prodMemory Float @db.Real - lastSuccessProvisionningVersion String? -} - -enum ProjectStatus { - initializing - created - failed - archived - warning -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/schema.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/schema.prisma deleted file mode 100644 index aadf7fea1..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/schema.prisma +++ /dev/null @@ -1,21 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DB_URL") -} - -model Log { - id String @id @default(uuid()) @db.Uuid - data Json - action String @default("") - userId String? @db.Uuid - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - requestId String? @db.VarChar(36) - projectId String? @db.Uuid - project Project? @relation(fields: [projectId], references: [id]) - user User? @relation(fields: [userId], references: [id]) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/token.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/token.prisma deleted file mode 100644 index c0c55751c..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/token.prisma +++ /dev/null @@ -1,30 +0,0 @@ -model AdminToken { - id String @id @unique @default(uuid()) @db.Uuid - name String - permissions BigInt - userId String @db.Uuid - expirationDate DateTime? - lastUse DateTime? - createdAt DateTime @default(now()) - status TokenStatus @default(active) - hash String - owner User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) -} - -model PersonalAccessToken { - id String @id @unique @default(uuid()) @db.Uuid - name String - userId String @db.Uuid - expirationDate DateTime - lastUse DateTime? - createdAt DateTime @default(now()) - status TokenStatus @default(active) - hash String - owner User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) -} - -enum TokenStatus { - active - revoked - inactive -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/topography.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/topography.prisma deleted file mode 100644 index ad8e3be22..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/topography.prisma +++ /dev/null @@ -1,53 +0,0 @@ -model Cluster { - id String @id @unique @default(uuid()) @db.Uuid - label String @unique @db.VarChar(50) - privacy ClusterPrivacy @default(dedicated) - secretName String @unique @default(uuid()) @db.VarChar(50) - clusterResources Boolean @default(false) - kubeConfigId String @unique @db.Uuid - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - infos String? @db.VarChar(1000) - external Boolean @default(false) - memory Float @db.Real - cpu Float @db.Real - gpu Float @db.Real - zoneId String @db.Uuid - kubeconfig Kubeconfig @relation(fields: [kubeConfigId], references: [id], onDelete: Cascade) - zone Zone @relation(fields: [zoneId], references: [id]) - environments Environment[] - projects Project[] @relation("ClusterToProject") - stages Stage[] @relation("ClusterToStage") -} - -model Kubeconfig { - id String @id @unique @default(uuid()) @db.Uuid - user Json - cluster Json - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - parentCluster Cluster? -} - -model Stage { - id String @id @unique @default(uuid()) @db.Uuid - name String @unique @db.VarChar - environments Environment[] - clusters Cluster[] @relation("ClusterToStage") -} - -model Zone { - id String @id @unique @default(uuid()) @db.Uuid - slug String @unique @db.VarChar(10) - label String @db.VarChar(50) - description String? @db.VarChar(200) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - argocdUrl String @default("https://example.com") - clusters Cluster[] -} - -enum ClusterPrivacy { - public - dedicated -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/user.prisma b/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/user.prisma deleted file mode 100644 index e90fb69f8..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/prisma/schema/user.prisma +++ /dev/null @@ -1,23 +0,0 @@ -model User { - id String @id @db.Uuid - firstName String - lastName String - email String @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - lastLogin DateTime? - adminRoleIds String[] - type UserType - - logs Log[] - projectsOwned Project[] - adminTokens AdminToken[] - projectMembers ProjectMembers[] - personalAccessTokens PersonalAccessToken[] -} - -enum UserType { - human - bot - ghost -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts deleted file mode 100644 index 7da66d6a4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.spec.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { AdminRole, User } from '@prisma/client' -import { faker } from '@faker-js/faker' -import prisma from '../../__mocks__/prisma' -import { BadRequest400 } from '../../utils/errors' -import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business' - -describe('test admin-role business', () => { - describe('listRoles', () => { - it('should stringify bigint', async () => { - const partialRole: Partial = { - permissions: 4n, - } - - prisma.adminRole.findMany.mockResolvedValueOnce([partialRole]) - const response = await listRoles() - expect(response).toEqual([{ permissions: '4' }]) - }) - }) - - describe('createRole', () => { - it('should create role with incremented position when position 0 is the highest', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 0, - } - - prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole) - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.adminRole.create.mockResolvedValue(null) - await createRole({ name: 'test' }) - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 1 } }) - }) - - it('should create role with incremented position with bigger position', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - } - - prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole) - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.adminRole.create.mockResolvedValue(null) - await createRole({ name: 'test' }) - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 51 } }) - }) - - it('should create role with incremented position with no role in db', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - } - - prisma.adminRole.findFirst.mockResolvedValueOnce(undefined) - prisma.adminRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.adminRole.create.mockResolvedValue(null) - await createRole({ name: 'test' }) - - expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 0 } }) - }) - }) - describe('deleteRole', () => { - const roleId = faker.string.uuid() - it('should delete role and remove id from concerned users', async () => { - const users = [{ - adminRoleIds: [roleId], - id: faker.string.uuid(), - }, { - adminRoleIds: [roleId, faker.string.uuid()], - id: faker.string.uuid(), - }] as const satisfies Partial[] - - prisma.user.findMany.mockResolvedValueOnce(users) - prisma.adminRole.findMany.mockResolvedValueOnce([]) - prisma.adminRole.create.mockResolvedValue(null) - await deleteRole(roleId) - - expect(prisma.user.update).toHaveBeenNthCalledWith(1, { where: { id: users[0].id }, data: { adminRoleIds: [] } }) - expect(prisma.user.update).toHaveBeenNthCalledWith(2, { where: { id: users[1].id }, data: { adminRoleIds: [users[1].adminRoleIds[1]] } }) - expect(prisma.adminRole.delete).toHaveBeenCalledWith({ where: { id: roleId } }) - }) - }) - describe('countRolesMembers', () => { - it('should return aggregated role member counts', async () => { - const partialRoles = [{ - id: faker.string.uuid(), - }, { - id: faker.string.uuid(), - }] as const satisfies Partial[] - - const users = [{ - adminRoleIds: [partialRoles[0].id, partialRoles[1].id], - }, { - adminRoleIds: [partialRoles[1].id], - }] as const satisfies Partial[] - prisma.adminRole.findMany.mockResolvedValue(partialRoles) - prisma.user.findMany.mockResolvedValue(users) - - const response = await countRolesMembers() - - expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 }) - }) - }) - describe('patchRoles', () => { - const dbRoles: AdminRole[] = [{ - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 0, - }, { - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 1, - }] - - it('should do nothing', async () => { - prisma.adminRole.findMany.mockResolvedValue([]) - await patchRoles([]) - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) - }) - - it('should return 400 if incoherent positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 1 }, - ] - prisma.adminRole.findMany.mockResolvedValue(dbRoles) - - const response = await patchRoles(updateRoles) - - expect(response).instanceOf(BadRequest400) - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) - }) - it('should return 400 if incoherent positions (missing roles)', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, position: 1 }, - ] - prisma.adminRole.findMany.mockResolvedValue(dbRoles) - - const response = await patchRoles(updateRoles) - - expect(response).instanceOf(BadRequest400) - expect(prisma.adminRole.update).toHaveBeenCalledTimes(0) - }) - it('should update positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 0 }, - ] - prisma.adminRole.findMany.mockResolvedValue(dbRoles) - - await patchRoles(updateRoles) - - expect(prisma.adminRole.update).toHaveBeenCalledTimes(2) - }) - it('should update permissions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, permissions: '0' }, - ] - prisma.adminRole.findMany.mockResolvedValue(dbRoles) - - await patchRoles(updateRoles) - - expect(prisma.adminRole.update).toHaveBeenCalledTimes(1) - expect(prisma.adminRole.update).toHaveBeenCalledWith({ - data: { - name: dbRoles[1].name, - oidcGroup: dbRoles[1].oidcGroup, - permissions: 0n, - position: 1, - }, - where: { - id: dbRoles[1].id, - }, - }) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts deleted file mode 100644 index b9af2b745..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/business.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Project, ProjectRole } from '@prisma/client' -import type { AdminRole, adminRoleContract } from '@cpn-console/shared' -import { - listAdminRoles, -} from '@old-server/resources/queries-index' -import type { ErrorResType } from '@old-server/utils/errors' -import { BadRequest400 } from '@old-server/utils/errors' -import prisma from '@old-server/prisma' - -export async function listRoles() { - return listAdminRoles() - .then(roles => roles.map(role => ({ ...role, permissions: role.permissions.toString() }))) -} - -export async function patchRoles(roles: typeof adminRoleContract.patchAdminRoles.body._type): Promise { - const dbRoles = await prisma.adminRole.findMany() - const positionsAvailable: number[] = [] - - const updatedRoles: (Omit & { permissions: bigint })[] = dbRoles - .filter(dbRole => roles.find(role => role.id === dbRole.id)) // filter non concerned dbRoles - .map((dbRole) => { - const matchingRole = roles.find(role => role.id === dbRole.id) - if (typeof matchingRole?.position !== 'undefined' && !positionsAvailable.includes(matchingRole.position)) { - positionsAvailable.push(matchingRole.position) - } - return { - id: dbRole.id, - name: matchingRole?.name ?? dbRole.name, - permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : dbRole.permissions, - position: matchingRole?.position ?? dbRole.position, - oidcGroup: matchingRole?.oidcGroup ?? dbRole.oidcGroup, - } - }) - - if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes') - for (const { id, ...role } of updatedRoles) { - await prisma.adminRole.update({ where: { id }, data: role }) - } - - return listRoles() -} - -export async function createRole(role: typeof adminRoleContract.createAdminRole.body._type) { - const dbMaxPosRole = (await prisma.adminRole.findFirst({ - orderBy: { position: 'desc' }, - select: { position: true }, - }))?.position ?? -1 - - await prisma.adminRole.create({ - data: { - ...role, - position: dbMaxPosRole + 1, - permissions: 0n, - }, - }) - - return listRoles() -} - -export async function countRolesMembers() { - const roles = await prisma.adminRole.findMany({ where: { oidcGroup: { equals: '' } }, select: { id: true } }) - const roleIds = roles.map(role => role.id) - const users = await prisma.user.findMany({ - where: { adminRoleIds: { hasSome: roleIds } }, - select: { adminRoleIds: true }, - }) - const rolesCounts: Record = Object.fromEntries(roles.map(role => [role.id, 0])) // {role uuid: 0} - for (const { adminRoleIds } of users) { - for (const roleId of adminRoleIds) { - rolesCounts[roleId]++ - } - } - return rolesCounts -} - -export async function deleteRole(roleId: Project['id']) { - const allUsers = await prisma.user.findMany({ - where: { - adminRoleIds: { has: roleId }, - }, - }) - for (const user of allUsers) { - await prisma.user.update({ - where: { id: user.id }, - data: { adminRoleIds: user.adminRoleIds.filter(adminRoleId => adminRoleId !== roleId) }, - }) - } - await prisma.adminRole.delete({ where: { id: roleId } }) - return null -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts deleted file mode 100644 index 3e35f78df..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/queries.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - AdminRole, - Prisma, -} from '@prisma/client' -import prisma from '@old-server/prisma' - -export const listAdminRoles = () => prisma.adminRole.findMany({ orderBy: { position: 'asc' } }) - -export function createAdminRole(data: Pick) { - return prisma.adminRole.create({ - data: { - name: data.name, - permissions: 0n, - position: data.position, - }, - }) -} - -export function updateAdminRole(id: AdminRole['id'], data: Pick) { - return prisma.projectRole.updateMany({ - where: { id }, - data, - }) -} - -export function deleteAdminRole(id: AdminRole['id']) { - return prisma.projectRole.delete({ - where: { - id, - }, - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts deleted file mode 100644 index e652c8262..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.spec.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { adminRoleContract } from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { BadRequest400 } from '../../utils/errors' -import { getUserMockInfos } from '../../utils/mocks' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListRolesMock = vi.spyOn(business, 'listRoles') -const businessCreateRoleMock = vi.spyOn(business, 'createRole') -const businessPatchRolesMock = vi.spyOn(business, 'patchRoles') -const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers') -const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole') - -describe('test adminRoleContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listAdminRoles', () => { - it('should return list of admin roles', async () => { - const roles = [{ id: faker.string.uuid(), name: 'Role 1', oidcGroup: '', position: 0, permissions: '1' }] - businessListRolesMock.mockResolvedValueOnce(roles) - - const response = await app.inject() - .get(adminRoleContract.listAdminRoles.path) - .end() - - expect(businessListRolesMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(roles) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('createAdminRole', () => { - it('should create a role for authorized users', async () => { - const user = getUserMockInfos(true) - const newRole = { id: 'newRole', name: 'New Role' } - const roleData = { name: 'New Role' } - - authUserMock.mockResolvedValueOnce(user) - businessCreateRoleMock.mockResolvedValueOnce(newRole) - - const response = await app.inject() - .post(adminRoleContract.createAdminRole.path) - .body(roleData) - .end() - - expect(businessCreateRoleMock).toHaveBeenCalledWith(roleData) - expect(response.json()).toEqual(newRole) - expect(response.statusCode).toEqual(201) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(adminRoleContract.createAdminRole.path) - .body({ name: 'New Role' }) - .end() - - expect(businessCreateRoleMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('patchAdminRoles', () => { - const updatedRoles = [{ id: faker.string.uuid(), name: 'Role 1', oidcGroup: '', position: 0, permissions: '1' }] - const rolesData = [{ id: updatedRoles[0].id, name: 'Updated Role' }] - it('should update roles for authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessPatchRolesMock.mockResolvedValueOnce(updatedRoles) - - const response = await app.inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end() - - expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData) - expect(response.json()).toEqual(updatedRoles) - expect(response.statusCode).toEqual(200) - }) - - it('should return error if business logic fails', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessPatchRolesMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - - const response = await app.inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end() - - expect(businessPatchRolesMock).toHaveBeenCalledWith(rolesData) - expect(response.statusCode).toEqual(400) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(adminRoleContract.patchAdminRoles.path) - .body(rolesData) - .end() - - expect(businessPatchRolesMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('adminRoleMemberCounts', () => { - it('should return counts of role members for admin', async () => { - const user = getUserMockInfos(true) - const counts = { role1: 5, role2: 3 } - - authUserMock.mockResolvedValueOnce(user) - businessCountRolesMembersMock.mockResolvedValueOnce(counts) - - const response = await app.inject() - .get(adminRoleContract.adminRoleMemberCounts.path) - .end() - - expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(counts) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 if user is not admin', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(adminRoleContract.adminRoleMemberCounts.path) - .end() - - expect(businessCountRolesMembersMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('deleteAdminRole', () => { - const roleId = faker.string.uuid() - it('should delete a role for authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessDeleteRoleMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .delete(adminRoleContract.deleteAdminRole.path.replace(':roleId', roleId)) - .end() - - expect(businessDeleteRoleMock).toHaveBeenCalledWith(roleId) - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(adminRoleContract.deleteAdminRole.path.replace(':roleId', roleId)) - .end() - - expect(businessDeleteRoleMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts deleted file mode 100644 index 9a5a81217..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-role/router.ts +++ /dev/null @@ -1,74 +0,0 @@ -// import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared' -// import { - // countRolesMembers, - // createRole, - // deleteRole, - // listRoles, - // patchRoles, -// } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' - -// export function adminRoleRouter() { - // return serverInstance.router(adminRoleContract, { - // // Récupérer des projets - // listAdminRoles: async () => { - // const body = await listRoles() - - // return { - // status: 200, - // body, - // } - // }, - - // createAdminRole: async ({ request: req, body }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const resBody = await createRole(body) - - // return { - // status: 201, - // body: resBody, - // } - // }, - - // patchAdminRoles: async ({ request: req, body }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const resBody = await patchRoles(body) - // if (resBody instanceof ErrorResType) return resBody - - // return { - // status: 200, - // body: resBody, - // } - // }, - - // adminRoleMemberCounts: async ({ request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const resBody = await countRolesMembers() - - // return { - // status: 200, - // body: resBody, - // } - // }, - - // deleteAdminRole: async ({ request: req, params }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const resBody = await deleteRole(params.roleId) - - // return { - // status: 204, - // body: resBody, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts deleted file mode 100644 index 83fa13fb4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { AdminToken } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import prisma from '../../__mocks__/prisma' -import { createToken, deleteToken, listTokens } from './business' - -describe('test admin-token business', () => { - describe('listTokens', () => { - it('should stringify bigint', async () => { - const partialtoken: Partial = { - permissions: 4n, - } - - prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]) - const response = await listTokens({}) - expect(response).toEqual([{ permissions: '4' }]) - }) - it('should return revoked', async () => { - const partialtoken: Partial = { - permissions: 4n, - status: 'revoked', - } - - prisma.adminToken.findMany.mockResolvedValueOnce([partialtoken]) - const response = await listTokens({ withRevoked: true }) - expect(response).toEqual([{ ...partialtoken, permissions: '4' }]) - }) - }) - - describe('createToken', () => { - it('should create ', async () => { - const dbToken: Partial = undefined - const userId = faker.string.uuid() - const createdToken: AdminToken = { - expirationDate: null, - id: faker.string.uuid(), - name: 'test', - permissions: '2', - } - prisma.adminToken.findUnique.mockResolvedValueOnce(dbToken) - prisma.adminToken.create.mockResolvedValueOnce(createdToken) - await createToken({ name: 'test', permissions: '2', expirationDate: null }, userId, undefined) - - expect(prisma.adminToken.create).toHaveBeenCalledWith({ - data: { - name: 'test', - hash: expect.any(String), - permissions: 2n, - userId: expect.any(String), - expirationDate: undefined, - }, - omit: expect.any(Object), - include: { - owner: true, - }, - }) - }) - it('should not create cause expiration is too short', async () => { - const expirationDate = new Date() - await createToken({ name: 'test', permissions: '2', expirationDate: expirationDate.toISOString() }) - - expect(prisma.adminToken.create).toHaveBeenCalledTimes(0) - }) - }) - - describe('deleteToken', () => { - it('should delete token', async () => { - prisma.adminToken.delete.mockResolvedValueOnce(undefined) - await deleteToken(faker.string.uuid()) - expect(prisma.adminToken.updateMany).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts deleted file mode 100644 index c5307fdca..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/business.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { createHash, randomUUID } from 'node:crypto' -import { type adminTokenContract, generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' -import type { $Enums, AdminToken, Prisma } from '@prisma/client' -import prisma from '../../prisma' -import { BadRequest400 } from '@old-server/utils/errors' - -export async function listTokens(query: typeof adminTokenContract.listAdminTokens.query._type) { - const where = { - status: { - in: ['active'] as $Enums.TokenStatus[], - }, - } as const satisfies Prisma.AdminTokenWhereInput - - if (query?.withRevoked) where.status.in.push('revoked') - - return prisma.adminToken.findMany({ - omit: { hash: true }, - include: { owner: true }, - orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], - where, - }).then(tokens => - tokens.map(({ permissions, ...token }) => ({ permissions: permissions.toString(), ...token })), - ) -} - -export async function createToken(data: typeof adminTokenContract.createAdminToken.body._type) { - if (data.expirationDate && !isAtLeastTomorrow(new Date(data.expirationDate))) { - return new BadRequest400('Date d\'expiration trop courte') - } - const password = generateRandomPassword(48, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') - const hash = createHash('sha256').update(password).digest('hex') - const botUserId = randomUUID() - await prisma.user.create({ - data: { - firstName: 'Bot Admin', - lastName: data.name, - type: 'bot', - id: botUserId, - email: `${botUserId}@bot.io`, - }, - }) - const token = await prisma.adminToken.create({ - data: { - ...data, - hash, - permissions: BigInt(data.permissions), - expirationDate: data.expirationDate ? new Date(data.expirationDate) : undefined, - userId: botUserId, - }, - omit: { hash: true }, - include: { owner: true }, - }) - return { - ...token, - password, - permissions: token.permissions.toString(), - } -} - -export async function deleteToken(id: AdminToken['id']) { - return prisma.adminToken.updateMany({ - where: { id }, - data: { - status: 'revoked', - expirationDate: new Date(Date.now()), - }, - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts deleted file mode 100644 index 92f371452..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.spec.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ExposedAdminToken } from '@cpn-console/shared' -import { adminTokenContract } from '@cpn-console/shared' -import type { AdminToken } from '@prisma/client' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getUserMockInfos } from '../../utils/mocks' -import { BadRequest400 } from '../../utils/errors' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListTokensMock = vi.spyOn(business, 'listTokens') -const businessCreateTokenMock = vi.spyOn(business, 'createToken') -const businessDeleteTokenMock = vi.spyOn(business, 'deleteToken') - -describe('test adminTokenContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listAdminTokens', () => { - it('should return list of admin tokens', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - const tokens: AdminToken[] = [{ - id: faker.string.uuid(), - name: 'token1', - permissions: '2', - lastUse: (new Date()).toISOString(), - expirationDate: null, - status: 'active', - createdAt: (new Date(Date.now())).toISOString(), - }] - businessListTokensMock.mockResolvedValueOnce(tokens) - - const response = await app.inject() - .get(adminTokenContract.listAdminTokens.path) - .end() - - expect(businessListTokensMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(tokens) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(adminTokenContract.listAdminTokens.path) - .end() - - expect(businessListTokensMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('createAdminToken', () => { - it('should create a token for authorized users', async () => { - const user = getUserMockInfos(true) - - const newToken = { - id: faker.string.uuid(), - name: 'test', - lastUse: null, - expirationDate: null, - password: faker.string.alpha({ casing: 'lower', length: 10 }), - permissions: '2', - createdAt: (new Date(Date.now())).toISOString(), - status: 'active', - } - const tokenData: ExposedAdminToken = { - name: newToken.name, - permissions: newToken.permissions, - expirationDate: null, - } - - authUserMock.mockResolvedValueOnce(user) - businessCreateTokenMock.mockResolvedValueOnce(newToken) - - const response = await app.inject() - .post(adminTokenContract.createAdminToken.path) - .body(tokenData) - .end() - - expect(businessCreateTokenMock).toHaveBeenCalledWith(tokenData) - expect(response.json()).toEqual(newToken) - expect(response.statusCode).toEqual(201) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(adminTokenContract.createAdminToken.path) - .body({ - name: 'new-token', - expirationDate: null, - permissions: '4', - }) - .end() - - expect(businessCreateTokenMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - - it('should pass business error', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessCreateTokenMock.mockResolvedValueOnce(new BadRequest400('Invalid date')) - - const response = await app.inject() - .post(adminTokenContract.createAdminToken.path) - .body({ - name: 'new-token', - expirationDate: null, - permissions: '4', - }) - .end() - - expect(businessCreateTokenMock).toHaveBeenCalledTimes(1) - expect(response.statusCode).toEqual(400) - }) - }) - - describe('deleteAdminToken', () => { - const tokenId = faker.string.uuid() - it('should delete a token for authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessDeleteTokenMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .delete(adminTokenContract.deleteAdminToken.path.replace(':tokenId', tokenId)) - .end() - - expect(businessDeleteTokenMock).toHaveBeenCalledWith(tokenId) - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(adminTokenContract.deleteAdminToken.path.replace(':tokenId', tokenId)) - .end() - - expect(businessDeleteTokenMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts deleted file mode 100644 index 3ee61ed10..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/admin-token/router.ts +++ /dev/null @@ -1,44 +0,0 @@ -// import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared' -// import { serverInstance } from '../../app' -// import { createToken, deleteToken, listTokens } from './business' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' - -// export function adminTokenRouter() { - // return serverInstance.router(adminTokenContract, { - // listAdminTokens: async ({ request: req, query }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - // const body = await listTokens(query) - - // return { - // status: 200, - // body, - // } - // }, - - // createAdminToken: async ({ request: req, body: data }) => { - // const perms = await authUser(req) - - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - // const body = await createToken(data) - // if (body instanceof ErrorResType) return body - - // return { - // status: 201, - // body, - // } - // }, - - // deleteAdminToken: async ({ request: req, params }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - // await deleteToken(params.tokenId) - - // return { - // status: 204, - // body: null, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts deleted file mode 100644 index 2280f9de8..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { faker } from '@faker-js/faker' -import type { Cluster, Environment } from '@prisma/client' -import prisma from '../../__mocks__/prisma' -import { hook } from '../../__mocks__/utils/hook-wrapper' -import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '../../utils/errors' -import { createCluster, deleteCluster, getClusterAssociatedEnvironments, getClusterDetails, getClusterUsage, listClusters, updateCluster } from './business' - -vi.mock('../../utils/hook-wrapper', async () => ({ - hook, -})) - -const userId = faker.string.uuid() -const requestId = faker.string.uuid() -const cluster: Cluster = { - id: faker.string.uuid(), - infos: faker.lorem.lines(2), - privacy: 'public', - createdAt: new Date(), - updatedAt: new Date(), - zoneId: faker.string.uuid(), - clusterResources: false, - kubeConfigId: faker.string.uuid(), - label: faker.string.alpha(10), - secretName: faker.string.alpha(10), - external: false, - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), -} -describe('test Cluster business logic', () => { - describe('listClusters', () => { - it('should filter for user', async () => { - prisma.cluster.findMany.mockResolvedValue([]) - await listClusters(userId) - expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1) - expect(prisma.cluster.findMany).toHaveBeenCalledWith({ select: expect.any(Object), where: { OR: [{ privacy: 'public' }, expect.any(Object), expect.any(Object), expect.any(Object)] } }) - }) - it('should not filter', async () => { - const dbStages = [{ id: faker.string.uuid() }] - prisma.cluster.findMany.mockResolvedValue([{ stages: dbStages }] as unknown as Cluster[]) - const response = await listClusters() - expect(prisma.cluster.findMany).toHaveBeenCalledTimes(1) - expect(prisma.cluster.findMany).toHaveBeenCalledWith({ select: expect.any(Object), where: {} }) - expect(response[0].stageIds).toStrictEqual([dbStages[0].id]) - }) - }) - - describe('getClusterAssociatedEnvironments', () => { - it('should list all environments attached to a cluster', async () => { - const envName = faker.string.alpha(8) - const projectName = faker.string.alpha(8) - const ownerEmail = faker.internet.email() - const cpu = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) - const gpu = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) - const memory = faker.number.float({ min: 0, max: 10, fractionDigits: 1 }) - const envs = [{ name: envName, cpu, gpu, memory, project: { name: projectName, owner: { email: ownerEmail } } }] as unknown as Environment[] - prisma.environment.findMany.mockResolvedValue(envs) - const response = await getClusterAssociatedEnvironments(cluster.id) - expect(response).toStrictEqual([{ - name: envName, - project: projectName, - owner: ownerEmail, - cpu, - gpu, - memory, - }]) - }) - }) - - describe('getClusterDetails', () => { - it('should return a cluster details', async () => { - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) - await getClusterDetails(cluster.id) - }) - it('should return a cluster details, without infos in db', async () => { - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, infos: null, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) - const response = await getClusterDetails(cluster.id) - expect(response.infos).toBe('') - }) - }) - - describe('getClusterUsage', () => { - it('should return a cluster usage', async () => { - prisma.environment.aggregate.mockResolvedValue({ _count: {}, _avg: {}, _min: {}, _max: {}, _sum: { - cpu: 10, - gpu: 5, - memory: 20, - } }) - const response = await getClusterUsage(cluster.id) - expect(response).toStrictEqual({ - cpu: 10, - gpu: 5, - memory: 20, - }) - }) - }) - - describe('createCluster', () => { - it('should create cluster', async () => { - hook.cluster.upsert.mockResolvedValue({ failed: false }) - prisma.cluster.findUnique.mockResolvedValue(null) - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) - prisma.cluster.create.mockResolvedValue(cluster) - - const response = await createCluster({ - infos: faker.string.alpha(10), - zoneId: faker.string.uuid(), - privacy: 'public', - stageIds: [], - clusterResources: false, - kubeconfig: { cluster: { tlsServerName: faker.internet.domainName() }, user: {} }, - label: faker.string.alpha(10), - external: false, - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - }, userId, requestId) - - expect(response).not.instanceOf(ErrorResType) - expect(prisma.cluster.create).toHaveBeenCalled() - }) - }) - - describe('updateCluster', () => { - it('should update cluster', async () => { - hook.cluster.upsert.mockResolvedValue({ failed: false }) - prisma.cluster.findUnique.mockResolvedValue(cluster) - prisma.cluster.findUniqueOrThrow.mockResolvedValue({ ...cluster, projects: [], stages: [], kubeconfig: { user: {}, cluster: {} } } as Cluster) - prisma.cluster.update.mockResolvedValue(cluster) - - const response = await updateCluster({ - infos: faker.string.alpha(10), - zoneId: faker.string.uuid(), - privacy: 'public', - stageIds: [], - }, cluster.id, userId, requestId) - - expect(response).not.instanceOf(ErrorResType) - expect(prisma.cluster.update).toHaveBeenCalled() - }) - it('should return 404', async () => { - prisma.cluster.findUnique.mockResolvedValue(null) - const response = await updateCluster({ infos: faker.string.alpha(10) }, cluster.id, userId, requestId) - expect(response).instanceOf(NotFound404) - }) - }) - - describe('deleteCluster', () => { - it('should delete cluster', async () => { - hook.cluster.delete.mockResolvedValue({}) - await deleteCluster({ clusterId: cluster.id, userId, requestId }) - - expect(prisma.cluster.delete).toHaveBeenCalledTimes(1) - }) - it('should return failed hook', async () => { - hook.cluster.delete.mockResolvedValue({ failed: true }) - const response = await deleteCluster({ clusterId: cluster.id, userId, requestId }) - - expect(response).instanceOf(Unprocessable422) - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) - }) - it('should not delete cluster, env attached', async () => { - prisma.environment.findFirst.mockResolvedValue({ id: faker.string.uuid() } as Environment) - const response = await deleteCluster({ clusterId: cluster.id, userId, requestId }) - - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) - expect(response).instanceOf(BadRequest400) - }) - }) -}) - -// findUniqueOrThrow diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts deleted file mode 100644 index b3e423a07..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/business.ts +++ /dev/null @@ -1,230 +0,0 @@ -import type { Prisma, Project, User } from '@prisma/client' -import type { Cluster, ClusterDetails, Kubeconfig, clusterContract } from '@cpn-console/shared' -import { ClusterDetailsSchema, ClusterPrivacy } from '@cpn-console/shared' -import { - addLogs, - createCluster as createClusterQuery, - deleteCluster as deleteClusterQuery, - getClusterById, - getClusterByLabel, - getClusterDetails as getClusterDetailsQuery, - getClusterEnvironments, - getProjectsByClusterId, - linkClusterToProjects, - linkZoneToClusters, - listClusters as listClustersQuery, - listStagesByClusterId, - removeClusterFromProject, - removeClusterFromStage, - updateCluster as updateClusterQuery, -} from '@old-server/resources/queries-index' -import { linkClusterToStages } from '@old-server/resources/stage/business' -import { validateSchema } from '@old-server/utils/business' -import { hook } from '@old-server/utils/hook-wrapper' -import { BadRequest400, ErrorResType, NotFound404, Unprocessable422 } from '@old-server/utils/errors' -import prisma from '@old-server/prisma' -import type { Resources } from '@old-server/types/index' - -export async function listClusters(userId?: User['id']) { - const where: Prisma.ClusterWhereInput = userId - ? { - OR: [ - // Sélectionne tous les clusters publics - { privacy: 'public' }, - // Sélectionne les clusters associés aux projets dont l'user est membre - { - projects: { some: { members: { some: { userId } } } }, - }, - // Sélectionne les clusters associés aux projets dont l'user est owner - { - projects: { some: { ownerId: userId } }, - }, - // Sélectionne les clusters associés aux environnments appartenant à des projets dont l'user est membre - { - environments: { some: { project: { members: { some: { userId } } } } }, - }, - ], - } - : {} - const clusters = await listClustersQuery(where) - return clusters.map(({ stages, ...cluster }) => ({ - ...cluster, - stageIds: stages.map(({ id }) => id), - })) -} - -export async function getClusterAssociatedEnvironments(clusterId: string) { - const clusterEnvironments = await getClusterEnvironments(clusterId) - - return clusterEnvironments.map((environment) => { - return ({ - project: environment.project?.name, - name: environment.name, - owner: environment.project.owner.email, - cpu: environment.cpu, - gpu: environment.gpu, - memory: environment.memory, - }) - }) -} - -export async function getClusterDetails(clusterId: string): Promise { - const { infos, projects, stages, kubeconfig, ...details } = await getClusterDetailsQuery(clusterId) - return { - ...details, - infos: infos ?? '', - projectIds: projects.map(project => project.id), - stageIds: stages.map(({ id }) => id), - kubeconfig: { - cluster: kubeconfig.cluster as unknown as Kubeconfig['cluster'], - user: kubeconfig.user as unknown as Kubeconfig['user'], - }, - } -} - -export async function getClusterUsage(clusterId: string): Promise { - const clusterUsage = await prisma.environment.aggregate({ - _sum: { - memory: true, - cpu: true, - gpu: true, - }, - where: { - clusterId, - }, - }) - return { - cpu: clusterUsage._sum.cpu ?? 0, - gpu: clusterUsage._sum.gpu ?? 0, - memory: clusterUsage._sum.memory ?? 0, - } -} - -export async function createCluster(data: typeof clusterContract.createCluster.body._type, userId: User['id'], requestId: string) { - const isLabelTaken = await getClusterByLabel(data.label) - if (isLabelTaken) return new BadRequest400('Ce label existe déjà pour un autre cluster') - - data.projectIds = data.privacy === ClusterPrivacy.PUBLIC - ? [] - : data.projectIds ?? [] - - const { - projectIds, - stageIds, - kubeconfig, - zoneId, - ...clusterData - } = data - - const clusterCreated = await createClusterQuery(clusterData, kubeconfig, zoneId) - - if (data.privacy === ClusterPrivacy.DEDICATED && projectIds.length) { - await linkClusterToProjects(clusterCreated.id, projectIds) - } - - if (stageIds?.length) { - await linkClusterToStages(clusterCreated.id, stageIds) - } - - const hookReply = await hook.cluster.upsert(clusterCreated.id, zoneId) - await addLogs({ action: 'Create Cluster', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la création du cluster') - } - - return getClusterDetails(clusterCreated.id) -} - -export async function updateCluster(data: typeof clusterContract.updateCluster.body._type, clusterId: Cluster['id'], userId: User['id'], requestId: string): Promise { - if (data?.privacy === ClusterPrivacy.PUBLIC) delete data.projectIds - - const schemaValidation = ClusterDetailsSchema.partial().safeParse({ ...data, id: clusterId }) - const validateResult = validateSchema(schemaValidation) - if (validateResult instanceof ErrorResType) return validateResult - - const dbCluster = await getClusterById(clusterId) - if (!dbCluster) return new NotFound404() - - const { - projectIds, - stageIds, - kubeconfig, - zoneId, - ...clusterData - } = data - - const clusterUpdated = await updateClusterQuery(clusterId, clusterData, - // @ts-ignore - kubeconfig) - - // zone - if (zoneId) { - await linkZoneToClusters(zoneId, [clusterId]) - } - - // projects - const dbProjects = await getProjectsByClusterId(clusterId) - - let projectsToRemove: Project['id'][] = [] - - if (projectIds && clusterUpdated.privacy === ClusterPrivacy.DEDICATED) { - await linkClusterToProjects(clusterId, projectIds) - projectsToRemove = dbProjects?.map(project => project.id)?.filter(dbProjectId => !projectIds.includes(dbProjectId)) ?? [] - } else if (clusterUpdated.privacy === ClusterPrivacy.PUBLIC) { - projectsToRemove = dbProjects?.map(project => project.id) ?? [] - } - - for (const projectId of projectsToRemove) { - await removeClusterFromProject(clusterUpdated.id, projectId) - } - - // stages - if (stageIds) { - await linkClusterToStages(clusterId, stageIds) - - const dbStages = await listStagesByClusterId(clusterId) - if (dbStages) { - for (const stage of dbStages) { - if (!stageIds.includes(stage.id)) { - await removeClusterFromStage(clusterUpdated.id, stage.id) - } - } - } - } - - const hookReply = await hook.cluster.upsert(clusterId, dbCluster.zoneId) - await addLogs({ action: 'Update Cluster', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la mise à jour du cluster') - } - - return getClusterDetails(clusterId) -} - -interface DeleteClusterArgs { - clusterId: Cluster['id'] - userId?: User['id'] - requestId: string - force?: boolean -} -export async function deleteCluster({ clusterId, requestId, force, userId }: DeleteClusterArgs) { - let message: string | null = null - if (force) { - const envs = await prisma.environment.deleteMany({ - where: { clusterId }, - }) - message = `${envs.count} environnements supprimés de force, n'oubliez pas de reprovisionner les projets concernés` - } else { - const environment = await prisma.environment.findFirst({ where: { clusterId } }) - if (environment) return new BadRequest400('Impossible de supprimer le cluster, des environnements en activité y sont déployés') - } - - const hookReply = await hook.cluster.delete(clusterId) - await addLogs({ action: 'Delete Cluster', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la suppression du cluster') - } - - await deleteClusterQuery(clusterId) - return message -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts deleted file mode 100644 index fad96194e..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/queries.ts +++ /dev/null @@ -1,312 +0,0 @@ -import type { Cluster, Environment, Kubeconfig, Prisma, Project, Stage } from '@prisma/client' -import prisma from '@old-server/prisma' - -export async function getClustersAssociatedWithProject(projectId: Project['id']) { - const [ - clusterIdsHistory, - clusterIdsEnv, - ] = await Promise.all([ - prisma.projectClusterHistory.findMany({ - select: { - clusterId: true, - }, - where: { - projectId, - }, - }).then(history => history.map(({ clusterId }) => clusterId)), - prisma.cluster.findMany({ - where: { environments: { some: { project: { id: projectId } } } }, - select: { id: true }, - }).then(cluster => cluster.map(({ id }) => id)), - ]) - const clusterIds = [ - ...clusterIdsHistory, - ...clusterIdsEnv.filter(id => !clusterIdsHistory.includes(id)), - ] - return prisma.cluster.findMany({ - where: { id: { in: clusterIds } }, - select: { - id: true, - infos: true, - label: true, - external: true, - privacy: true, - secretName: true, - kubeconfig: true, - clusterResources: true, - cpu: true, - gpu: true, - memory: true, - zone: { - select: { - id: true, - slug: true, - argocdUrl: true, - label: true, - }, - }, - }, - }) -} - -export async function updateProjectClusterHistory(projectId: Project['id'], clusterIds: Cluster['id'][]) { - return prisma.$transaction([ - prisma.projectClusterHistory.deleteMany({ - where: { - AND: { - projectId, - clusterId: { notIn: clusterIds }, - }, - }, - }), - prisma.projectClusterHistory.createMany({ - data: clusterIds.map(clusterId => ({ clusterId, projectId })), - skipDuplicates: true, - }), - ]) -} - -export function getClusterById(id: Cluster['id']) { - return prisma.cluster.findUnique({ - where: { id }, - include: { kubeconfig: true }, - }) -} - -export function getClusterByIdOrThrow(id: Cluster['id']) { - return prisma.cluster.findUniqueOrThrow({ - where: { id }, - include: { kubeconfig: true, zone: true }, - }) -} - -export function getClusterEnvironments(clusterId: Cluster['id']) { - return prisma.environment.findMany({ - where: { clusterId }, - select: { - name: true, - cpu: true, - gpu: true, - memory: true, - project: { - select: { - slug: true, - name: true, - owner: true, - members: true, - }, - }, - }, - }) -} - -export function getClusterDetails(id: Cluster['id']) { - return prisma.cluster.findUniqueOrThrow({ - where: { id }, - select: { - createdAt: true, - projects: { - select: { - id: true, - }, - }, - id: true, - clusterResources: true, - infos: true, - external: true, - label: true, - privacy: true, - kubeconfig: true, - stages: true, - updatedAt: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, - }, - }) -} - -export function getClustersByIds(clusterIds: Cluster['id'][]) { - return prisma.cluster.findMany({ - where: { - id: { in: clusterIds }, - }, - include: { kubeconfig: true }, - }) -} - -export function getPublicClusters() { - return prisma.cluster.findMany({ - where: { privacy: 'public' }, - include: { zone: true }, - }) -} - -export async function getClusterNamesByZoneId(zoneId: string) { - const clusterNames = await prisma.cluster.findMany({ - where: { zoneId }, - select: { - label: true, - }, - }) - return clusterNames.map(({ label }) => label) -} - -export function getClusterByLabel(label: Cluster['label']) { - return prisma.cluster.findUnique({ where: { label } }) -} - -export function getClusterByEnvironmentId(id: Environment['id']) { - return prisma.cluster.findMany({ - where: { - environments: { - some: { id }, - }, - }, - include: { kubeconfig: true }, - }) -} - -export function getClustersWithProjectIdAndConfig() { - return prisma.cluster.findMany({ - select: { - id: true, - stages: true, - projects: { - where: { - status: { not: 'archived' }, - }, - select: { - id: true, - name: true, - slug: true, - status: true, - }, - }, - clusterResources: true, - label: true, - infos: true, - privacy: true, - secretName: true, - kubeconfig: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, - }, - }) -} - -export function listClusters(where: Prisma.ClusterWhereInput) { - return prisma.cluster.findMany({ - where, - select: { - id: true, - label: true, - stages: true, - clusterResources: true, - privacy: true, - infos: true, - external: true, - zoneId: true, - cpu: true, - gpu: true, - memory: true, - }, - }) -} - -export async function getProjectsByClusterId(id: Cluster['id']) { - return (await prisma.cluster.findUniqueOrThrow({ - where: { id }, - select: { projects: true }, - }))?.projects -} - -export async function listStagesByClusterId(id: Cluster['id']) { - return (await prisma.cluster.findUniqueOrThrow({ - where: { id }, - select: { stages: true }, - }))?.stages -} - -export function createCluster(data: Omit, kubeconfig: Pick, zoneId: string) { - return prisma.cluster.create({ - data: { - ...data, - // @ts-ignore - kubeconfig: { create: kubeconfig }, - zone: { - connect: { id: zoneId }, - }, - }, - }) -} - -export function updateCluster(id: Cluster['id'], data: Partial>, kubeconfig: Pick) { - return prisma.cluster.update({ - where: { id }, - data: { - ...data, - kubeconfig: { - // @ts-ignore - update: kubeconfig, - }, - }, - }) -} - -export function linkClusterToProjects(id: Cluster['id'], projectIds: Project['id'][]) { - return prisma.cluster.update({ - where: { id }, - data: { - projects: { - connect: projectIds.map(projectId => ({ id: projectId })), - }, - }, - }) -} - -export function linkClusterToStages(id: Cluster['id'], stageIds: Stage['id'][]) { - return prisma.cluster.update({ - where: { id }, - data: { - stages: { - connect: stageIds.map(stageId => ({ id: stageId })), - }, - }, - }) -} - -export function removeClusterFromProject(id: Cluster['id'], projectId: Project['id']) { - return prisma.cluster.update({ - where: { id }, - data: { - projects: { - disconnect: { - id: projectId, - }, - }, - }, - }) -} - -export function removeClusterFromStage(id: Cluster['id'], stageId: Stage['id']) { - return prisma.cluster.update({ - where: { id }, - data: { - stages: { - disconnect: { - id: stageId, - }, - }, - }, - }) -} - -export function deleteCluster(id: Cluster['id']) { - return prisma.cluster.delete({ - where: { id }, - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts deleted file mode 100644 index c4085a889..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.spec.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ClusterDetails, Environment } from '@cpn-console/shared' -import { clusterContract } from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getUserMockInfos } from '../../utils/mocks' -import { BadRequest400 } from '../../utils/errors' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMock = vi.spyOn(business, 'listClusters') -const businessGetDetailsMock = vi.spyOn(business, 'getClusterDetails') -const businessGetUsageMock = vi.spyOn(business, 'getClusterUsage') -const businessGetEnvironmentsMock = vi.spyOn(business, 'getClusterAssociatedEnvironments') -const businessCreateMock = vi.spyOn(business, 'createCluster') -const businessUpdateMock = vi.spyOn(business, 'updateCluster') -const businessDeleteMock = vi.spyOn(business, 'deleteCluster') - -describe('test clusterContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - describe('listClusters', () => { - it('as non admin', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - businessListMock.mockResolvedValueOnce([]) - const response = await app.inject() - .get(clusterContract.listClusters.path) - .end() - - expect(businessListMock).toHaveBeenCalledWith(user.user.id) - - expect(response.json()).toStrictEqual([]) - expect(response.statusCode).toEqual(200) - }) - it('as admin', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - - businessListMock.mockResolvedValueOnce([]) - const response = await app.inject() - .get(clusterContract.listClusters.path) - .end() - - expect(businessListMock).toHaveBeenCalledWith() - - expect(response.json()).toStrictEqual([]) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('getClusterDetails', () => { - it('should return cluster details', async () => { - const cluster: ClusterDetails = { - id: faker.string.uuid(), - clusterResources: true, - infos: '', - external: false, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - } - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetDetailsMock.mockResolvedValueOnce(cluster) - const response = await app.inject() - .get(clusterContract.getClusterDetails.path.replace(':clusterId', cluster.id)) - .end() - - expect(businessGetDetailsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(cluster) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(clusterContract.getClusterDetails.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetDetailsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getClusterUsage', () => { - it('should return cluster usage', async () => { - const resources = { - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - } - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetUsageMock.mockResolvedValueOnce(resources) - const response = await app.inject() - .get(clusterContract.getClusterUsage.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetUsageMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(resources) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(clusterContract.getClusterUsage.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetUsageMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getClusterEnvironments', () => { - it('should return cluster environments', async () => { - const envs: Environment[] = [] - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetEnvironmentsMock.mockResolvedValueOnce(envs) - const response = await app.inject() - .get(clusterContract.getClusterEnvironments.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(clusterContract.getClusterEnvironments.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('createCluster', () => { - const cluster: ClusterDetails = { - id: faker.string.uuid(), - clusterResources: true, - infos: '', - external: true, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - } - - it('should return created cluster', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(cluster) - const response = await app.inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end() - - expect(response.json()).toEqual(cluster) - expect(response.statusCode).toEqual(201) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(clusterContract.createCluster.path) - .body(cluster) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('updateCluster', () => { - const clusterId = faker.string.uuid() - const cluster: Omit = { - clusterResources: true, - infos: '', - external: false, - label: faker.string.alpha(), - privacy: 'public', - stageIds: [], - zoneId: faker.string.uuid(), - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - kubeconfig: { - cluster: { tlsServerName: faker.string.alpha() }, - user: {}, - }, - } - - it('should return created cluster', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: clusterId, ...cluster }) - const response = await app.inject() - .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) - .body(cluster) - .end() - - expect(response.json()).toEqual({ id: clusterId, ...cluster }) - expect(response.statusCode).toEqual(200) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) - .body(cluster) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(clusterContract.updateCluster.path.replace(':clusterId', clusterId)) - .body(cluster) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('deleteCluster', () => { - it('should return empty when delete', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(clusterContract.deleteCluster.path.replace(':clusterId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts deleted file mode 100644 index 211f753b5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/cluster/router.ts +++ /dev/null @@ -1,125 +0,0 @@ -// import type { AsyncReturnType } from '@cpn-console/shared' -// import { AdminAuthorized, clusterContract } from '@cpn-console/shared' -// import { - // createCluster, - // deleteCluster, - // getClusterAssociatedEnvironments, - // getClusterDetails as getClusterDetailsBusiness, - // getClusterUsage, - // listClusters, - // updateCluster, -// } from './business' -// import '@old-server/types/index' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' - -// export function clusterRouter() { - // return serverInstance.router(clusterContract, { - // listClusters: async ({ request: req }) => { - // const { adminPermissions, user } = await authUser(req) - - // let body: AsyncReturnType = [] - // if (AdminAuthorized.isAdmin(adminPermissions)) { - // body = await listClusters() - // } else if (user) { - // body = await listClusters(user.id) - // } - - // return { - // status: 200, - // body, - // } - // }, - - // getClusterDetails: async ({ params, request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const clusterId = params.clusterId - // const cluster = await getClusterDetailsBusiness(clusterId) - - // return { - // status: 200, - // body: cluster, - // } - // }, - - // getClusterUsage: async ({ params, request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const clusterId = params.clusterId - // const usage = await getClusterUsage(clusterId) - - // return { - // status: 200, - // body: usage, - // } - // }, - - // createCluster: async ({ request: req, body: data }) => { - // const { adminPermissions, user } = await authUser(req) - // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - - // if (!user) return new Unauthorized401('Require to be requested from user not api key') - // const body = await createCluster(data, user.id, req.id) - // if (body instanceof ErrorResType) return body - - // return { - // status: 201, - // body, - // } - // }, - - // getClusterEnvironments: async ({ request: req, params }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const clusterId = params.clusterId - // const environments = await getClusterAssociatedEnvironments(clusterId) - - // return { - // status: 200, - // body: environments, - // } - // }, - - // updateCluster: async ({ request: req, params, body: data }) => { - // const { user, adminPermissions } = await authUser(req) - // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - // if (!user) return new Unauthorized401('Require to be requested from user not api key') - - // const clusterId = params.clusterId - // const body = await updateCluster(data, clusterId, user.id, req.id) - - // if (body instanceof ErrorResType) return body - - // return { - // status: 200, - // body, - // } - // }, - - // deleteCluster: async ({ request: req, params, query: { force } }) => { - // const { user, adminPermissions, tokenId } = await authUser(req) - // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - // if (!user?.id && !tokenId) return new Unauthorized401('Your identity has not been found') - - // const clusterId = params.clusterId - // const body = await deleteCluster({ - // clusterId, - // userId: user?.id, - // requestId: req.id, - // force, - // }) - - // if (body instanceof ErrorResType) return body - - // return { - // status: 204, - // body, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts deleted file mode 100644 index 36104efa3..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.spec.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Cluster, Environment, Project, ProjectMembers, ProjectRole, Stage, User } from '@prisma/client' -import prisma from '../../__mocks__/prisma' -import { hook } from '../../__mocks__/utils/hook-wrapper' -import { checkClusterResources, checkProjectResources, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business' -import { Result } from '../../utils/business' - -vi.mock('../../utils/hook-wrapper', async () => ({ - hook, -})) - -const user: User = { - id: faker.string.uuid(), - createdAt: new Date(), - updatedAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - adminRoleIds: [], - type: 'human', - lastLogin: null, -} -const project: Project & { - clusters: Pick[] - members: ProjectMembers[] - roles: ProjectRole[] - owner: User -} = { - createdAt: new Date(), - updatedAt: new Date(), - description: '', - everyonePerms: 649n, - id: faker.string.uuid(), - locked: false, - name: faker.string.alphanumeric(8), - status: 'created', - ownerId: faker.string.uuid(), - owner: user, - limitless: false, - hprodCpu: faker.number.int({ min: 0, max: 1000 }), - hprodGpu: faker.number.int({ min: 0, max: 1000 }), - hprodMemory: faker.number.int({ min: 0, max: 1000 }), - prodCpu: faker.number.int({ min: 0, max: 1000 }), - prodGpu: faker.number.int({ min: 0, max: 1000 }), - prodMemory: faker.number.int({ min: 0, max: 1000 }), - clusters: [], - roles: [], - members: [], - slug: faker.string.alphanumeric(8), - lastSuccessProvisionningVersion: faker.string.numeric(), -} - -describe('test environment business', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('getProjectEnvironments', () => { - it('should query environment for projectId', async () => { - prisma.environment.findMany.mockResolvedValue([]) - const projectId = faker.string.uuid() - await getProjectEnvironments(projectId) - - expect(prisma.environment.findMany).toHaveBeenCalledTimes(1) - }) - }) - - describe('createEnvironment', () => { - const clusterId = faker.string.uuid() - const stageId = faker.string.uuid() - const env = { name: 'new-env' } - it('should create environment and trigger hook', async () => { - const requestId = faker.string.uuid() - const stageId = faker.string.uuid() - - prisma.environment.create.mockResolvedValue({ clusterId } as Environment) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const result = await createEnvironment({ - userId: user.id, - projectId: project.id, - name: env.name, - cpu: 0.1, - gpu: 0.5, - memory: 2.0, - clusterId, - stageId, - requestId, - }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - - it('should create environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid() - - prisma.environment.create.mockResolvedValue({ clusterId } as Environment) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const result = await createEnvironment({ - userId: user.id, - projectId: project.id, - name: env.name, - cpu: 0.1, - gpu: 0.5, - memory: 2.0, - clusterId, - stageId, - requestId, - }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - }) - }) - - describe('updateEnvironment', () => { - it('should update environment and trigger hook', async () => { - const requestId = faker.string.uuid() - const environmentId = faker.string.uuid() - - prisma.environment.update.mockResolvedValue({ projectId: project.id } as Environment) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const result = await updateEnvironment({ - user, - environmentId, - requestId, - cpu: 2.0, - gpu: 4.0, - memory: 12.5, - }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.update).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - - it('should update environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid() - const environmentId = faker.string.uuid() - - prisma.environment.update.mockResolvedValue({ projectId: project.id } as Environment) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const result = await updateEnvironment({ - user, - environmentId, - requestId, - cpu: 2.0, - gpu: 4.0, - memory: 12.5, - }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.update).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - }) - }) - - describe('deleteEnvironment', () => { - it('should delete environment and trigger hook', async () => { - const requestId = faker.string.uuid() - const environmentId = faker.string.uuid() - - prisma.environment.delete.mockResolvedValue({ projectId: project.id } as Environment) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const result = await deleteEnvironment({ environmentId, userId: user.id, projectId: project.id, requestId }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.delete).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - - it('should delete environment and trigger hook but hooks failed', async () => { - const requestId = faker.string.uuid() - const environmentId = faker.string.uuid() - - prisma.environment.delete.mockResolvedValue({ projectId: project.id } as Environment) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const result = await deleteEnvironment({ environmentId, userId: user.id, projectId: project.id, requestId }) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(prisma.environment.delete).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - }) - }) - - describe('checkClusterResources', () => { - it('should authorize cluster not yet configured', async () => { - const cluster: Cluster = { - cpu: 0, - gpu: 0, - memory: 0, - } as Cluster - const result = await checkClusterResources({ cpu: 1, gpu: 0, memory: 1 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - it('should authorize cluster not yet used', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 0, - gpu: 0, - memory: 0, - }, - } as any) - const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 7 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - it('should authorize cluster used but not full', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 2, - gpu: 0, - memory: 2, - }, - } as any) - const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 6 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - it('should refuse cluster without enough space', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 5, - gpu: 0, - memory: 5, - }, - } as any) - const result = await checkClusterResources({ cpu: 8, gpu: 0, memory: 6 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - expect(result.error).toEqual('Le cluster ne dispose pas de suffisamment de ressources : CPU, Mémoire.') - }) - it('should refuse cluster without GPU', async () => { - const cluster: Cluster = { - cpu: 10, - gpu: 0, - memory: 8, - } as Cluster - prisma.environment.aggregate.mockResolvedValue({ - _sum: { - cpu: 2, - gpu: 0, - memory: 2, - }, - } as any) - const result = await checkClusterResources({ cpu: 2, gpu: 1, memory: 2 }, cluster) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - expect(result.error).toEqual('Le cluster ne dispose pas de suffisamment de ressources : GPU.') - }) - }) - - describe('checkProjectResources', () => { - const prodStage: Stage = { - id: faker.string.uuid(), - name: 'prod', - } - const hprodStage: Stage = { - id: faker.string.uuid(), - name: 'hprod', - } - it('should authorize prod deployment for project with hprod resource but no prod resources', async () => { - const project: Project = { - hprodCpu: 10, - hprodGpu: 10, - hprodMemory: 10, - prodCpu: 0, - prodGpu: 0, - prodMemory: 0, - } as Project - prisma.stage.findUnique.mockResolvedValue(prodStage) - prisma.stage.findMany.mockResolvedValue([prodStage]) - const result = await checkProjectResources({ cpu: 1, gpu: 0, memory: 1, stageId: prodStage.id }, project) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeTruthy() - }) - it('should refuse hprod deployment for project with hprod resource but no prod resources', async () => { - const project: Project = { - hprodCpu: 10, - hprodGpu: 10, - hprodMemory: 10, - prodCpu: 0, - prodGpu: 0, - prodMemory: 0, - } as Project - prisma.stage.findUnique.mockResolvedValue(hprodStage) - prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]) - prisma.environment.aggregate.mockResolvedValue({ - _sum: { cpu: 0, gpu: 0, memory: 0 }, - } as any) - const result = await checkProjectResources({ cpu: 20, gpu: 20, memory: 20, stageId: hprodStage.id }, project) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - expect(result.error).toEqual('Le projet ne dispose pas de suffisamment de ressources : CPU, GPU, Mémoire.') - }) - it('should refuse overloading hprod deployment', async () => { - const project: Project = { - hprodCpu: 20, - hprodGpu: 20, - hprodMemory: 20, - prodCpu: 10, - prodGpu: 10, - prodMemory: 10, - } as Project - prisma.stage.findUnique.mockResolvedValue(hprodStage) - prisma.stage.findMany.mockResolvedValue([prodStage] as Stage[]) - prisma.environment.aggregate.mockResolvedValue({ - _sum: { cpu: 15, gpu: 15, memory: 15 }, - } as any) - const result = await checkProjectResources({ cpu: 5, gpu: 6, memory: 5, stageId: hprodStage.id }, project) - expect(result).toBeInstanceOf(Result) - expect(result.success).toBeFalsy() - expect(result.error).toEqual('Le projet ne dispose pas de suffisamment de ressources : GPU.') - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts deleted file mode 100644 index 83131d848..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/business.ts +++ /dev/null @@ -1,300 +0,0 @@ -import type { Cluster, Environment, Project, Stage, User } from '@prisma/client' -import { - addLogs, - deleteEnvironment as deleteEnvironmentQuery, - getEnvironmentsByProjectId, - initializeEnvironment, - updateEnvironment as updateEnvironmentQuery, -} from '@old-server/resources/queries-index' -import type { Resources, UserDetails } from '@old-server/types/index' -import { hook } from '@old-server/utils/hook-wrapper' -import prisma from '@old-server/prisma' -import { Result } from '@old-server/utils/business' - -export function getProjectEnvironments(projectId: Project['id']) { - return getEnvironmentsByProjectId(projectId) -} - -// Routes logic -interface CreateEnvironmentParam { - userId: User['id'] - projectId: Project['id'] - name: Environment['name'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] - clusterId: Environment['clusterId'] - stageId: Stage['id'] - requestId: string -} - -interface CreateEnvironmentResult { - id: Environment['id'] - createdAt: Date - updatedAt: Date - projectId: Project['id'] - name: Environment['name'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] - clusterId: Environment['clusterId'] - stageId: Stage['id'] -} - -export async function createEnvironment({ - userId, - projectId, - name, - cpu, - gpu, - memory, - clusterId, - stageId, - requestId, -}: CreateEnvironmentParam): Promise> { - const environment = await initializeEnvironment({ projectId, name, cpu, gpu, memory, clusterId, stageId }) - - const { results } = await hook.project.upsert(projectId) - await addLogs({ action: 'Create Environment', data: results, userId, requestId, projectId }) - if (results.failed) { - return Result.fail('Echec des services à la création de l\'environnement') - } - - return Result.succeed({ - ...environment, - stageId, - }) -} - -interface UpdateEnvironmentParam { - user: UserDetails - environmentId: Environment['id'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] - requestId: string -} - -export async function updateEnvironment({ - user, - environmentId, - requestId, - cpu, - gpu, - memory, -}: UpdateEnvironmentParam) { - const env = await updateEnvironmentQuery({ - id: environmentId, - cpu, - gpu, - memory, - }) - const { results } = await hook.project.upsert(env.projectId) - await addLogs({ action: 'Update Environment', data: results, userId: user.id, requestId, projectId: env.projectId }) - if (results.failed) { - return Result.fail('Echec des services à la mise à jour de l\'environnement') - } - - return Result.succeed(env) -} - -interface DeleteEnvironmentParam { - userId?: User['id'] - environmentId: Environment['id'] - projectId: Project['id'] - requestId: string -} - -export async function deleteEnvironment({ - userId, - environmentId, - projectId, - requestId, -}: DeleteEnvironmentParam) { - const env = await deleteEnvironmentQuery(environmentId) - - const { results } = await hook.project.upsert(projectId) - await addLogs({ action: 'Delete Environment', data: results, userId, requestId, projectId: env.projectId }) - if (results.failed) { - return Result.fail('Echec des services à la suppression de l\'environnement') - } - return Result.succeed(null) -} - -export async function checkEnvironmentCreate(input: { - clusterId: Cluster['id'] - projectId: Project['id'] - name: Environment['name'] - stageId: Stage['id'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] -}): Promise> { - const errorMessages: string[] = [] - const [stage, sameNameEnvironment, cluster] = await Promise.all([ - input.stageId - ? prisma.stage.findUnique({ where: { id: input.stageId } }) - : undefined, - input.name - ? prisma.environment.findUnique({ where: { projectId_name: { projectId: input.projectId, name: input.name } } }) - : undefined, - input.clusterId - ? prisma.cluster.findFirst({ - where: { - OR: [{ // un cluster public - id: input.clusterId, - privacy: 'public', - }, { - id: input.clusterId, // un cluster dédié rattaché au projet - privacy: 'dedicated', - projects: { some: { id: input.projectId } }, - }], - }, - }) - : undefined, - ]) - if (sameNameEnvironment) errorMessages.push('Ce nom d\'environnement est déjà pris.') - if (!stage) errorMessages.push('Type d\'environnment invalide.') - if (!cluster) { - errorMessages.push('Cluster invalide.') - } else { - const resourceCheckResult = await checkClusterResources(input, cluster) - if (resourceCheckResult.isError) { - errorMessages.push(resourceCheckResult.error) - } - const project = await prisma.project.findUniqueOrThrow({ where: { id: input.projectId } }) - const projectCheckResult = await checkProjectResources(input, project) - if (projectCheckResult.isError) { - errorMessages.push(projectCheckResult.error) - } - } - if (errorMessages.length > 0) { - return Result.fail(errorMessages.join('\n')) - } - return Result.succeed(true) -} - -export async function checkClusterResources(input: { - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] -}, cluster: Cluster): Promise> { - if (cluster.cpu === 0 && cluster.memory === 0) { - // Unconfigured cluster - return Result.succeed(true) - } - const unsufficientResource = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { cpu: cluster.cpu, gpu: cluster.gpu, memory: cluster.memory }, - where: { - cluster: { - id: cluster.id, - }, - }, - }) - if (unsufficientResource.length > 0) { - return Result.fail(`Le cluster ne dispose pas de suffisamment de ressources : ${unsufficientResource.join(', ')}.`) - } - return Result.succeed(true) -} - -export async function checkProjectResources(input: { - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] - stageId: Environment['stageId'] -}, project: Project): Promise> { - if (project.limitless) { - // No limits - return Result.succeed(true) - } - const stage = await prisma.stage.findUnique({ where: { id: input.stageId } }) - const prodStages = await prisma.stage.findMany({ select: { id: true }, where: { name: 'prod' } }) - let overflowResources: string[] - if (stage?.name === 'prod') { - overflowResources = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { cpu: project.prodCpu, gpu: project.prodGpu, memory: project.prodMemory }, - where: { - projectId: project.id, - stageId: { - in: prodStages.map(s => s.id), - }, - }, - }) - } else { // hprod - overflowResources = await getOverflowResources({ - request: { cpu: input.cpu, gpu: input.gpu, memory: input.memory }, - limit: { cpu: project.hprodCpu, gpu: project.hprodGpu, memory: project.hprodMemory }, - where: { - projectId: project.id, - stageId: { - notIn: prodStages.map(s => s.id), - }, - }, - }) - } - if (overflowResources.length > 0) { - return Result.fail(`Le projet ne dispose pas de suffisamment de ressources : ${overflowResources.join(', ')}.`) - } - return Result.succeed(true) -} - -export async function checkEnvironmentUpdate(input: { - environmentId: Environment['id'] - cpu: Environment['cpu'] - gpu: Environment['gpu'] - memory: Environment['memory'] -}): Promise> { - const environment = await prisma.environment.findUniqueOrThrow({ - select: { cluster: true, projectId: true, stageId: true }, - where: { id: input.environmentId }, - }) - const cluster = await prisma.cluster.findUniqueOrThrow({ - where: { id: environment.cluster.id }, - }) - const errorMessages: string[] = [] - const resourceCheckResult = await checkClusterResources(input, cluster) - if (resourceCheckResult.isError) { - errorMessages.push(resourceCheckResult.error) - } - const project = await prisma.project.findUniqueOrThrow({ where: { id: environment.projectId } }) - const projectCheckResult = await checkProjectResources({ stageId: environment.stageId, ...input }, project) - if (projectCheckResult.isError) { - errorMessages.push(projectCheckResult.error) - } - if (errorMessages.length > 0) { - return Result.fail(errorMessages.join('\n')) - } - return Result.succeed(true) -} - -export async function getOverflowResources({ request, limit, where }: { - request: Resources - limit: Resources - where: any -}): Promise { - if (limit.cpu === 0 && limit.memory === 0) { - // Unconfigured project prod resources - return [] - } - const environmentResources = await prisma.environment.aggregate({ - _sum: { - memory: true, - cpu: true, - gpu: true, - }, - where, - }) - const unsufficientResource: string[] = [] - if ((environmentResources._sum.cpu ?? 0) + request.cpu > limit.cpu) { - unsufficientResource.push('CPU') - } - if ((environmentResources._sum.gpu ?? 0) + request.gpu > limit.gpu) { - unsufficientResource.push('GPU') - } - if ((environmentResources._sum.memory ?? 0) + request.memory > limit.memory) { - unsufficientResource.push('Mémoire') - } - return unsufficientResource -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts deleted file mode 100644 index 78a68da69..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/queries.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Environment, Prisma, Project } from '@prisma/client' -import prisma from '@old-server/prisma' - -// SELECT -export function getEnvironmentByIdOrThrow(id: Environment['id']) { - return prisma.environment.findUniqueOrThrow({ where: { id }, include: { stage: true } }) -} - -export function getEnvironmentInfos(id: Environment['id']) { - return prisma.environment.findUniqueOrThrow({ - where: { id }, - include: { - project: { - select: { - owner: true, - name: true, - id: true, - status: true, - repositories: { - where: { isInfra: true }, - }, - locked: true, - clusters: { - select: { - id: true, - label: true, - privacy: true, - clusterResources: true, - }, - }, - }, - }, - stage: true, - }, - }) -} - -export async function getEnvironmentsByProjectId(projectId: Project['id']) { - return prisma.environment.findMany({ - where: { projectId }, - include: { - stage: true, - }, - }) -} - -export function getEnvironmentByIdWithCluster(id: Environment['id']) { - return prisma.environment.findUnique({ - where: { id }, - include: { - cluster: { - include: { kubeconfig: true }, - }, - }, - }) -} - -// INSERT -export function initializeEnvironment(data: Prisma.EnvironmentUncheckedCreateInput) { - return prisma.environment.create({ - data, - include: { - project: { - include: { - repositories: { - where: { isInfra: true }, - }, - }, - }, - }, - }) -} - -export function updateEnvironment({ id, cpu, gpu, memory }: { id: Environment['id'], cpu: Environment['cpu'], gpu: Environment['gpu'], memory: Environment['memory'] }) { - return prisma.environment.update({ - where: { - id, - }, - data: { - cpu, - gpu, - memory, - }, - }) -} - -// DELETE -export function deleteEnvironment(id: Environment['id']) { - return prisma.environment.delete({ - where: { id }, - }) -} - -export function deleteAllEnvironmentForProject(id: Project['id']) { - return prisma.environment.deleteMany({ - where: { projectId: id }, - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts deleted file mode 100644 index 764b6c6f1..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { type Environment, PROJECT_PERMS, environmentContract } from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { atDates, getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetProjectEnvironmentsMock = vi.spyOn(business, 'getProjectEnvironments') -const businessCreateEnvironmentMock = vi.spyOn(business, 'createEnvironment') -const businessUpdateEnvironmentMock = vi.spyOn(business, 'updateEnvironment') -const businessDeleteEnvironmentMock = vi.spyOn(business, 'deleteEnvironment') -const businessCheckEnvironmentCreateMock = vi.spyOn(business, 'checkEnvironmentCreate') -const businessCheckEnvironmentUpdateMock = vi.spyOn(business, 'checkEnvironmentUpdate') - -describe('environmentRouter tests', () => { - let projectId: string - let environmentId: string - let environmentData: Omit - - beforeEach(() => { - vi.resetAllMocks() - projectId = faker.string.uuid() - environmentId = faker.string.uuid() - environmentData = { - projectId, - name: 'envname', - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - clusterId: faker.string.uuid(), - stageId: faker.string.uuid(), - } - }) - - describe('listEnvironments', () => { - it('should return environments for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetProjectEnvironmentsMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(environmentContract.listEnvironments.path) - .query({ projectId }) - .end() - - expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledWith(projectId) - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should return empty for non member of projectId query ', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(environmentContract.listEnvironments.path) - .query({ projectId }) - .end() - - expect(businessGetProjectEnvironmentsMock).toHaveBeenCalledTimes(0) - expect(response.json()).toEqual([]) - }) - }) - - describe('createEnvironment', () => { - it('should create environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ success: true }) - businessCreateEnvironmentMock.mockResolvedValueOnce({ - success: true, - data: { id: environmentId, ...environmentData, ...atDates }, - }) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.json()).toMatchObject({ id: environmentId, ...environmentData }) - expect(response.statusCode).toEqual(201) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(403) - }) - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ success: true, message: 'pas d erreur' }) - businessCreateEnvironmentMock.mockResolvedValueOnce({ isError: true, message: 'une erreur' }) - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(500) - }) - it('should pass invalid reason error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentCreateMock.mockResolvedValueOnce({ isError: true, message: 'une erreur' }) - const response = await app.inject() - .post(environmentContract.createEnvironment.path) - .body(environmentData) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('updateEnvironment', () => { - let updateData: { cpu: number, gpu: number, memory: number } - beforeEach(() => { - updateData = { - cpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - gpu: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - memory: faker.number.float({ min: 0, max: 10, fractionDigits: 1 }), - } - }) - it('should update environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ success: true, value: true }) - businessUpdateEnvironmentMock.mockResolvedValueOnce({ success: true, data: { id: environmentId, ...environmentData, ...atDates } }) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.json()).toMatchObject({ id: environmentId, ...environmentData }) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 404 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(404) - }) - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateEnvironmentMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(500) - }) - it('should pass invalid reason error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCheckEnvironmentUpdateMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) - const response = await app.inject() - .put(environmentContract.updateEnvironment.path.replace(':environmentId', environmentId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('deleteEnvironment', () => { - it('should delete environment for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteEnvironmentMock.mockResolvedValueOnce({ success: true }) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(204) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteEnvironmentMock.mockResolvedValueOnce({ isError: true, value: 'une erreur' }) - const response = await app.inject() - .delete(environmentContract.deleteEnvironment.path.replace(':environmentId', environmentId)) - .end() - - expect(response.statusCode).toEqual(500) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts deleted file mode 100644 index 1fb9950a1..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/environment/router.ts +++ /dev/null @@ -1,109 +0,0 @@ -// import { ProjectAuthorized, environmentContract } from '@cpn-console/shared' -// import { checkEnvironmentCreate, checkEnvironmentUpdate, createEnvironment, deleteEnvironment, getProjectEnvironments, updateEnvironment } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { BadRequest400, Forbidden403, Internal500, NotFound404, Unauthorized401 } from '@old-server/utils/errors' - -// export function environmentRouter() { - // return serverInstance.router(environmentContract, { - // listEnvironments: async ({ request: req, query }) => { - // const projectId = query.projectId - // const perms = await authUser(req, { id: projectId }) - - // const environments = ProjectAuthorized.ListEnvironments(perms) - // ? await getProjectEnvironments(projectId) - // : [] - - // return { - // status: 200, - // body: environments, - // } - // }, - - // createEnvironment: async ({ request: req, body: requestBody }) => { - // const projectId = requestBody.projectId - // const perms = await authUser(req, { id: projectId }) - - // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - // if (!perms.projectPermissions) return new NotFound404() - // if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const checkCreateResult = await checkEnvironmentCreate({ ...requestBody }) - // if (checkCreateResult.isError) return new BadRequest400(checkCreateResult.error) - - // const result = await createEnvironment({ - // userId: perms.user.id, - // projectId, - // name: requestBody.name, - // clusterId: requestBody.clusterId, - // cpu: requestBody.cpu, - // gpu: requestBody.gpu, - // memory: requestBody.memory, - // stageId: requestBody.stageId, - // requestId: req.id, - // }) - // if (result.isError) { - // return new Internal500(result.error) - // } - // return { - // status: 201, - // body: result.data, - // } - // }, - - // updateEnvironment: async ({ request: req, body: requestBody, params }) => { - // const { environmentId } = params - // const perms = await authUser(req, { environmentId }) - // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - // if (!ProjectAuthorized.ListEnvironments(perms)) return new NotFound404() - // if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const checkUpdateResult = await checkEnvironmentUpdate({ environmentId, ...requestBody }) - // if (checkUpdateResult.isError) return new BadRequest400(checkUpdateResult.error) - - // const result = await updateEnvironment({ - // user: perms.user, - // environmentId, - // cpu: requestBody.cpu, - // gpu: requestBody.gpu, - // memory: requestBody.memory, - // requestId: req.id, - // }) - // if (result.isError) { - // return new Internal500(result.error) - // } - // return { - // status: 200, - // body: result.data, - // } - // }, - - // deleteEnvironment: async ({ request: req, params }) => { - // const { environmentId } = params - // const perms = await authUser(req, { environmentId }) - // if (!perms.projectPermissions) return new NotFound404() - // if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const result = await deleteEnvironment({ - // userId: perms.user?.id, - // environmentId, - // requestId: req.id, - // projectId: perms.projectId, - // }) - // if (result.isError) { - // return new Internal500(result.error) - // } - - // return { - // status: 204, - // body: result.data, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts deleted file mode 100644 index 29f166eda..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -// import type { FastifyInstance } from 'fastify' -// import { serverInstance } from '@old-server/app' - -// import { adminRoleRouter } from './admin-role/router' -// import { adminTokenRouter } from './admin-token/router' -// import { clusterRouter } from './cluster/router' -// import { environmentRouter } from './environment/router' -// import { logRouter } from './log/router' -// import { personalAccessTokenRouter } from './user/tokens/router' -// import { pluginConfigRouter } from './system/config/router' -// import { projectMemberRouter } from './project-member/router' -// import { projectRoleRouter } from './project-role/router' -// import { projectRouter } from './project/router' -// import { projectServiceRouter } from './project-service/router' -// import { repositoryRouter } from './repository/router' -// import { serviceChainRouter } from './service-chain/router' -// import { serviceMonitorRouter } from './service-monitor/router' -// import { stageRouter } from './stage/router' -// import { systemRouter } from './system/router' -// import { systemSettingsRouter } from './system/settings/router' -// import { userRouter } from './user/router' -// import { zoneRouter } from './zone/router' - -// // relax validation schema if NO_VALIDATION env var is set to true. -// // /!\ It can lead to security leaks !!!! -// const validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' } -// export function apiRouter() { - // return async (app: FastifyInstance) => { - // await app.register(serverInstance.plugin(adminRoleRouter()), validateTrue) - // await app.register(serverInstance.plugin(adminTokenRouter()), validateTrue) - // await app.register(serverInstance.plugin(clusterRouter()), validateTrue) - // await app.register(serverInstance.plugin(serviceChainRouter()), validateTrue) - // await app.register(serverInstance.plugin(environmentRouter()), validateTrue) - // await app.register(serverInstance.plugin(logRouter()), validateTrue) - // await app.register(serverInstance.plugin(personalAccessTokenRouter()), validateTrue) - // await app.register(serverInstance.plugin(projectRouter()), validateTrue) - // await app.register(serverInstance.plugin(projectMemberRouter()), validateTrue) - // await app.register(serverInstance.plugin(projectRoleRouter()), validateTrue) - // await app.register(serverInstance.plugin(projectServiceRouter()), validateTrue) - // await app.register(serverInstance.plugin(repositoryRouter()), validateTrue) - // await app.register(serverInstance.plugin(serviceMonitorRouter()), validateTrue) - // await app.register(serverInstance.plugin(pluginConfigRouter()), validateTrue) - // await app.register(serverInstance.plugin(stageRouter()), validateTrue) - // await app.register(serverInstance.plugin(systemRouter()), validateTrue) - // await app.register(serverInstance.plugin(systemSettingsRouter()), validateTrue) - // await app.register(serverInstance.plugin(userRouter()), validateTrue) - // await app.register(serverInstance.plugin(zoneRouter()), validateTrue) - // } -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts deleted file mode 100644 index 751c67991..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { faker } from '@faker-js/faker' -import prisma from '../../__mocks__/prisma' -import { getLogs } from './business' - -describe('test log business', () => { - it('should map filter (clean logs)', async () => { - const dbLogs = [{ - data: { args: {} }, - createdAt: new Date(), - updatedAt: new Date(), - userId: null, - action: 'Action', - id: faker.string.uuid(), - }] - const query = { limit: 10, offset: 10, clean: true, projectId: undefined } - prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]) - const [_total, logs] = await getLogs(query) - - expect(logs[0]).not.haveOwnProperty('requestId') - expect(logs[0].data).not.haveOwnProperty('results') - expect(logs[0].data).not.haveOwnProperty('args') - expect(logs[0].data).not.haveOwnProperty('config') - }) - - it('should not filter (admin logs)', async () => { - const dbLogs = [{ - data: { args: {} }, - createdAt: new Date(), - updatedAt: new Date(), - userId: null, - action: 'Action', - id: faker.string.uuid(), - }] - const query = { limit: 10, offset: 10, clean: false, projectId: undefined } - prisma.$transaction.mockResolvedValueOnce([dbLogs.length, dbLogs]) - const [_total, logs] = await getLogs(query) - - expect(logs[0].data).haveOwnProperty('args') - expect(logs[0].data).not.haveOwnProperty('config') - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts deleted file mode 100644 index 1ebfa48f8..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/business.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { logContract } from '@cpn-console/shared' -import { CleanLogSchema } from '@cpn-console/shared' -import { getAllLogs } from '@old-server/resources/queries-index' - -export async function getLogs({ offset, limit, projectId, clean }: typeof logContract.getLogs.query._type) { - const [total, logs] = await getAllLogs({ skip: offset, take: limit, where: { projectId } }) - return [ - total, - clean - ? logs.map(log => CleanLogSchema.parse(log)) - : logs, - ] -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts deleted file mode 100644 index 9ca1ea52d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/queries.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { Log, Prisma, Project, User } from '@prisma/client' -import { exclude } from '@cpn-console/shared' -import prisma from '@old-server/prisma' - -// SELECT -export function getAllLogsForUser(user: User, offset = 0) { - return prisma.log.findMany({ - where: { userId: user.id }, - take: 100, - skip: offset, - }) -} - -export function getAllLogs({ skip = 0, take = 5, where }: Prisma.LogFindManyArgs) { - return prisma.$transaction([ - prisma.log.count({ where }), - prisma.log.findMany({ - orderBy: { - createdAt: 'desc', - }, - skip, - take, - where, - }), - ]) -} - -// CREATE -interface AddLogsArgs { - action: Log['action'] - data: Record - userId?: User['id'] | null - requestId: string - projectId?: Project['id'] -} -export function addLogs({ action, data, requestId, userId = null, projectId }: AddLogsArgs) { - return prisma.log.create({ - data: { - action, - userId, - data: exclude(data, ['cluster', 'user', 'newCreds', 'apis']), - requestId, - projectId, - }, - }) -} - -// TECH -export function _createLog(data: Parameters[0]['create']) { - return prisma.log.upsert({ - where: { - id: data.id, - }, - create: data, - update: data, - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts deleted file mode 100644 index 1e50ba574..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { logContract } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetLogsMock = vi.spyOn(business, 'getLogs') - -describe('test logContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('getLogs', () => { - it('should return logs for admin', async () => { - const user = getUserMockInfos(true) - const logs = [] - const total = 1 - - authUserMock.mockResolvedValueOnce(user) - businessGetLogsMock.mockResolvedValueOnce([total, logs]) - - const response = await app.inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0 }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetLogsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ total, logs }) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for non-admin, no projectId', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0 }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetLogsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - - it('should return logs for non-admin, with projectId', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 1n }) - const user = getUserMockInfos(false, undefined, projectPerms) - const projectId = faker.string.uuid() - - const logs = [] - const total = 1 - - businessGetLogsMock.mockResolvedValueOnce([total, logs]) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0, projectId, clean: false }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetLogsMock).toHaveBeenCalledWith({ clean: true, limit: 10, offset: 0, projectId }) - expect(response.statusCode).toEqual(200) - }) - - it('should not return logs for non-admin, with projectId', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - const projectId = faker.string.uuid() - - const logs = [] - const total = 1 - - businessGetLogsMock.mockResolvedValueOnce([total, logs]) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(logContract.getLogs.path) - .query({ limit: 10, offset: 0, projectId, clean: false }) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts deleted file mode 100644 index 7894fdcfc..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/log/router.ts +++ /dev/null @@ -1,32 +0,0 @@ -// import type { CleanLog, Log, XOR } from '@cpn-console/shared' -// import { AdminAuthorized, logContract } from '@cpn-console/shared' -// import { getLogs } from './business' -// import { serverInstance } from '@old-server/app' -// import type { UserProfile, UserProjectProfile } from '@old-server/utils/controller' -// import { authUser } from '@old-server/utils/controller' -// import { Forbidden403 } from '@old-server/utils/errors' - -// export function logRouter() { - // return serverInstance.router(logContract, { - // // Récupérer des logs - // getLogs: async ({ request: req, query }) => { - // const perms: XOR = query.projectId - // ? await authUser(req, { id: query.projectId }) - // : await authUser(req) - - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { - // if (!perms.projectPermissions) { - // return new Forbidden403() - // } - // query.clean = true - // } - - // const [total, logs] = await getLogs(query) as [number, unknown[]] as [number, Array] - - // return { - // status: 200, - // body: { total, logs }, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts deleted file mode 100644 index 84a2c3758..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/business.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Project, User } from '@prisma/client' -import type { XOR, projectMemberContract } from '@cpn-console/shared' -import { UserSchema } from '@cpn-console/shared' -import { logViaSession } from '../user/business' -import { - addLogs, - deleteMember, - listMembers as listMembersQuery, - upsertMember, -} from '@old-server/resources/queries-index' -import prisma from '@old-server/prisma' -import { BadRequest400, NotFound404 } from '@old-server/utils/errors' -import { hook } from '@old-server/utils/hook-wrapper' - -export const listMembers = async (projectId: Project['id']) => listMembersQuery(projectId) - -export async function addMember(projectId: Project['id'], user: XOR<{ userId: string }, { email: string }>, requestorId: User['id'], requestId: string, projectOwnerId: Project['ownerId']) { - let userInDb: User | undefined | null - - if (user.userId) { - userInDb = await prisma.user.findUnique({ where: { id: user.userId, type: 'human' } }) - } else if (user.email) { - userInDb = await prisma.user.findUnique({ where: { email: user.email, type: 'human' } }) - } else { - return new BadRequest400('Veuillez spécifiez au moins un userId ou un email') - } - if (userInDb) { - if (userInDb.id === projectOwnerId) return new BadRequest400('Le owner ne peut pas être ajouté à cette liste') - } else if (user.email) { - const hookReply = await hook.user.retrieveUserByEmail(user.email) - await addLogs({ action: 'Retrieve User By Email', data: hookReply, userId: requestorId, requestId }) - if (hookReply.failed) { - throw new BadRequest400('Echec de la recherche auprès des services externes') - } - - const retrievedUser = hookReply.results.keycloak?.user - if (!retrievedUser) return new BadRequest400('Utilisateur introuvable') - const userValidated = UserSchema.pick({ email: true, firstName: true, lastName: true, id: true }).safeParse(retrievedUser) - if (!userValidated.success) return new BadRequest400('L\'utilisateur trouvé ne remplit pas les conditions de vérification') - const logResults = await logViaSession({ ...userValidated.data, groups: [] }) - userInDb = logResults.user - } else { - return new NotFound404() - } - - await upsertMember({ projectId, userId: userInDb.id, roleIds: [] }) - return listMembers(projectId) -} - -export async function patchMembers(projectId: Project['id'], members: typeof projectMemberContract.patchMembers.body._type) { - for (const member of members) { - await upsertMember({ projectId, userId: member.userId, roleIds: member.roles }) - } - return listMembers(projectId) -} - -export async function removeMember(projectId: Project['id'], userId: User['id']) { - await deleteMember({ projectId, userId }) - return listMembers(projectId) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts deleted file mode 100644 index 478e1a4b0..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/queries.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { - Prisma, - - Project, -} from '@prisma/client' - -import prisma from '@old-server/prisma' - -export const listMembers = (projectId: Project['id']) => prisma.projectMembers.findMany({ where: { projectId }, include: { user: true } }) - -export function upsertMember(data: Prisma.ProjectMembersUncheckedCreateInput) { - return prisma.projectMembers.upsert({ - where: { - projectId_userId: { - userId: data.userId, - projectId: data.projectId, - }, - }, - create: data, - update: { - roleIds: data.roleIds, - }, - include: { user: true }, - }) -} - -export function deleteMember(data: Prisma.ProjectMembersWhereUniqueInput['projectId_userId']) { - return prisma.projectMembers.delete({ - where: { - projectId_userId: data, - }, - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts deleted file mode 100644 index 9edee86a7..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.spec.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Member } from '@cpn-console/shared' -import { PROJECT_PERMS, projectMemberContract } from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' -import { BadRequest400 } from '../../utils/errors' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMembersMock = vi.spyOn(business, 'listMembers') -const businessAddMemberMock = vi.spyOn(business, 'addMember') -const businessPatchMembersMock = vi.spyOn(business, 'patchMembers') -const businessRemoveMemberMock = vi.spyOn(business, 'removeMember') - -describe('projectMemberRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const projectId = faker.string.uuid() - const userId = faker.string.uuid() - - describe('listMembers', () => { - it('should return members for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessListMembersMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectMemberContract.listMembers.path.replace(':projectId', projectId)) - .end() - - expect(businessListMembersMock).toHaveBeenCalledWith(projectId) - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectMemberContract.listMembers.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - }) - - describe('addMember', () => { - const memberData: Partial = { - userId: faker.string.uuid(), - } - - it('should add member for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - const newMember = { - ...memberData, - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - roleIds: [], - } - - businessAddMemberMock.mockResolvedValueOnce([newMember]) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.json()).toEqual([newMember]) - expect(response.statusCode).toEqual(201) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - businessAddMemberMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectMemberContract.addMember.path.replace(':projectId', projectId)) - .body(memberData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) - - describe('patchMembers', () => { - const patchData = [{ userId: faker.string.uuid(), roles: [] }] - - it('should patch members for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessPatchMembersMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectMemberContract.patchMembers.path.replace(':projectId', projectId)) - .body(patchData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) - - describe('removeMember', () => { - it('should remove member for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessRemoveMemberMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should be able leave a project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessRemoveMemberMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectMemberContract.removeMember.path.replace(':projectId', projectId).replace(':userId', userId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts deleted file mode 100644 index 900debd22..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-member/router.ts +++ /dev/null @@ -1,82 +0,0 @@ -// import { AdminAuthorized, ProjectAuthorized, projectMemberContract } from '@cpn-console/shared' -// import { - // addMember, - // listMembers, - // patchMembers, - // removeMember, -// } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' - -// export function projectMemberRouter() { - // return serverInstance.router(projectMemberContract, { - // listMembers: async ({ request: req, params }) => { - // const { projectId } = params - // const perms = await authUser(req, { id: projectId }) - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - // const body = await listMembers(projectId) - - // return { - // status: 200, - // body, - // } - // }, - - // addMember: async ({ request: req, params, body }) => { - // const { projectId } = params - // const perms = await authUser(req, { id: projectId }) - - // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - // if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const resBody = await addMember(projectId, body, perms.user.id, req.id, perms.projectOwnerId) - // if (resBody instanceof ErrorResType) return resBody - - // return { - // status: 201, - // body: resBody, - // } - // }, - - // patchMembers: async ({ request: req, params, body }) => { - // const { projectId } = params - // const perms = await authUser(req, { id: projectId }) - - // if (!perms.projectPermissions) return new NotFound404() - // if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const resBody = await patchMembers(projectId, body) - - // return { - // status: 200, - // body: resBody, - // } - // }, - - // removeMember: async ({ request: req, params }) => { - // const { projectId, userId } = params - // const perms = await authUser(req, { id: projectId }) - - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - // if (!ProjectAuthorized.ManageMembers(perms) && userId !== perms.user?.id) return new Forbidden403() - - // const resBody = await removeMember(projectId, params.userId) - - // return { - // status: 200, - // body: resBody, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts deleted file mode 100644 index e78bdd54b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { faker } from '@faker-js/faker' -import { describe, expect, it } from 'vitest' -import type { ProjectMembers, ProjectRole, User } from '@prisma/client' -import prisma from '../../__mocks__/prisma' -import { BadRequest400 } from '../../utils/errors' -import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business' - -const projectId = faker.string.uuid() -describe('test project-role business', () => { - describe('listRoles', () => { - it('should stringify bigint', async () => { - const partialRole: Partial = { - permissions: 4n, - } - - prisma.projectRole.findMany.mockResolvedValueOnce([partialRole]) - const response = await listRoles(projectId) - expect(response).toEqual([{ permissions: '4' }]) - }) - }) - - describe('createRole', () => { - it('should create role with incremented position when position 0 is the highest', async () => { - const dbRole: Partial = { - projectId, - permissions: 4n, - position: 0, - } - - prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole) - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.projectRole.create.mockResolvedValue(null) - await createRole(projectId, { name: 'test', permissions: '4' }) - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 1, projectId } }) - }) - - it('should create role with incremented position with bigger position', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - } - - prisma.projectRole.findFirst.mockResolvedValueOnce(dbRole) - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.projectRole.create.mockResolvedValue(null) - await createRole(projectId, { name: 'test', permissions: '4' }) - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 51, projectId } }) - }) - - it('should create role with incremented position with no role in db', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - } - - prisma.projectRole.findFirst.mockResolvedValueOnce(undefined) - prisma.projectRole.findMany.mockResolvedValueOnce([dbRole]) - prisma.projectRole.create.mockResolvedValue(null) - await createRole(projectId, { name: 'test', permissions: '4' }) - - expect(prisma.projectRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 4n, position: 0, projectId } }) - }) - }) - - describe('deleteRole', () => { - const roleId = faker.string.uuid() - it('should delete role and remove id from concerned users', async () => { - const dbRole: Partial = { - permissions: 4n, - position: 50, - id: faker.string.uuid(), - } - const members = [{ - userId: faker.string.uuid(), - projectId, - roleIds: [roleId], - }, { - userId: faker.string.uuid(), - projectId, - roleIds: [roleId, faker.string.uuid()], - }] as const satisfies Partial[] - - prisma.projectMembers.findMany.mockResolvedValueOnce(members) - prisma.projectRole.findMany.mockResolvedValueOnce([]) - prisma.projectRole.delete.mockResolvedValue(dbRole) - await deleteRole(roleId) - - expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(1, { where: expect.any(Object), data: { roleIds: { set: [] } } }) - expect(prisma.projectMembers.update).toHaveBeenNthCalledWith(2, { where: expect.any(Object), data: { roleIds: { set: [members[1].roleIds[1]] } } }) - expect(prisma.projectRole.delete).toHaveBeenCalledWith({ where: { id: roleId } }) - }) - }) - describe.skip('countRolesMembers', () => { - it('should return aggregated role member counts', async () => { - const partialRoles = [{ - id: faker.string.uuid(), - }, { - id: faker.string.uuid(), - }] as const satisfies Partial[] - - const users = [{ - projectRoleIds: [partialRoles[0].id, partialRoles[1].id], - }, { - projectRoleIds: [partialRoles[1].id], - }] as const satisfies Partial[] - prisma.projectRole.findMany.mockResolvedValue(partialRoles) - prisma.user.findMany.mockResolvedValue(users) - - const response = await countRolesMembers() - - expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 }) - }) - }) - describe('patchRoles', () => { - const dbRoles: ProjectRole[] = [{ - id: faker.string.uuid(), - name: faker.company.name(), - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 0, - projectId, - }, { - id: faker.string.uuid(), - name: faker.company.name(), - permissions: faker.number.bigInt({ min: 0n, max: 50000n }), - position: 1, - projectId, - }] - - it('should do nothing', async () => { - prisma.projectRole.findMany.mockResolvedValue([]) - await patchRoles(projectId, []) - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) - }) - - it('should return 400 if incoherent positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 1 }, - ] - prisma.projectRole.findMany.mockResolvedValue(dbRoles) - - const response = await patchRoles(projectId, updateRoles) - - expect(response).instanceOf(BadRequest400) - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) - }) - - it('should return 400 if incoherent positions (missing)', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, position: 1 }, - ] - prisma.projectRole.findMany.mockResolvedValue(dbRoles) - - const response = await patchRoles(projectId, updateRoles) - - expect(response).instanceOf(BadRequest400) - expect(prisma.projectRole.update).toHaveBeenCalledTimes(0) - }) - - it('should update positions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[0].id, position: 1 }, - { id: dbRoles[1].id, position: 0 }, - ] - prisma.projectRole.findMany.mockResolvedValue(dbRoles) - - await patchRoles(projectId, updateRoles) - - expect(prisma.projectRole.update).toHaveBeenCalledTimes(2) - }) - - it('should update permissions', async () => { - const updateRoles: Pick = [ - { id: dbRoles[1].id, permissions: '0' }, - ] - prisma.projectRole.findMany.mockResolvedValue(dbRoles) - - await patchRoles(projectId, updateRoles) - - expect(prisma.projectRole.update).toHaveBeenCalledTimes(1) - expect(prisma.projectRole.update).toHaveBeenCalledWith({ - data: { - name: dbRoles[1].name, - permissions: 0n, - position: 1, - }, - where: { - id: dbRoles[1].id, - }, - }) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts deleted file mode 100644 index 2bae2b7ed..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/business.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { projectRoleContract } from '@cpn-console/shared' -import type { Project, ProjectRole } from '@prisma/client' -import { - deleteRole as deleteRoleQuery, - listMembers, - listRoles as listRolesQuery, - updateRole, -} from '@old-server/resources/queries-index' -import { BadRequest400 } from '@old-server/utils/errors' -import prisma from '@old-server/prisma' - -export async function listRoles(projectId: Project['id']) { - return listRolesQuery(projectId) - .then(roles => roles.map(role => ({ ...role, permissions: role.permissions.toString() }))) -} - -export async function patchRoles(projectId: Project['id'], roles: typeof projectRoleContract.patchProjectRoles.body._type) { - const dbRoles = await listRoles(projectId) - const positionsAvailable: number[] = [] - - const updatedRoles = dbRoles - .filter(dbRole => roles.find(role => role.id === dbRole.id)) // filter non concerned dbRoles - .map((dbRole) => { - const matchingRole = roles.find(role => role.id === dbRole.id) - if (typeof matchingRole?.position !== 'undefined' && !positionsAvailable.includes(matchingRole.position)) { - positionsAvailable.push(matchingRole.position) - } - return { - id: matchingRole?.id ?? dbRole.id, - name: matchingRole?.name ?? dbRole.name, - permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : BigInt(dbRole.permissions), - position: matchingRole?.position ?? dbRole.position, - } - }) - if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes') - for (const { id, ...role } of updatedRoles) { - await updateRole(id, role) - } - - return listRoles(projectId) -} - -export async function createRole(projectId: Project['id'], role: typeof projectRoleContract.createProjectRole.body._type) { - const dbMaxPosRole = (await prisma.projectRole.findFirst({ - where: { projectId }, - orderBy: { position: 'desc' }, - select: { position: true }, - }))?.position ?? -1 - - await prisma.projectRole.create({ - data: { - ...role, - projectId, - position: dbMaxPosRole + 1, - permissions: BigInt(role.permissions), - }, - }) - - return listRoles(projectId) -} - -export async function countRolesMembers(projectId: Project['id']) { - const roles = await listRoles(projectId) - const members = await listMembers(projectId) - const rolesCounts: Record = Object.fromEntries(roles.map(role => [role.id, 0])) // {role uuid: 0} - for (const { roleIds } of members) { - for (const roleId of roleIds) { - rolesCounts[roleId]++ - } - } - return rolesCounts -} - -export async function deleteRole(roleId: Project['id']) { - await deleteRoleQuery(roleId) - return null -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts deleted file mode 100644 index 08c374294..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/queries.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { - Prisma, - Project, - - ProjectRole, -} from '@prisma/client' - -import prisma from '@old-server/prisma' - -export const listRoles = (projectId: Project['id']) => prisma.projectRole.findMany({ where: { projectId }, orderBy: { position: 'asc' } }) - -export function createRole(data: Pick) { - return prisma.projectRole.create({ - data: { - name: data.name, - permissions: 0n, - position: data.position, - projectId: data.projectId, - }, - }) -} - -export function updateRole(id: ProjectRole['id'], data: Pick) { - return prisma.projectRole.update({ - where: { id }, - data, - }) -} - -export async function deleteRole(id: ProjectRole['id']) { - const role = await prisma.projectRole.delete({ - where: { - id, - }, - }) - const attachedMembers = await prisma.projectMembers.findMany({ - where: { projectId: role.projectId, roleIds: { has: id } }, - }) - for (const member of attachedMembers) { - await prisma.projectMembers.update({ - where: { - projectId_userId: { - projectId: role.projectId, - userId: member.userId, - }, - }, - data: { - roleIds: { - set: member.roleIds.filter(roleId => roleId !== id), - }, - }, - }) - } -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts deleted file mode 100644 index ccdb1cb4b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { faker } from '@faker-js/faker' -import { PROJECT_PERMS, projectRoleContract } from '@cpn-console/shared' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' -import { BadRequest400 } from '../../utils/errors' -import * as business from './business' - -vi.mock('./business') -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) - -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessCreateRoleMock = vi.spyOn(business, 'createRole') -const businessDeleteRoleMock = vi.spyOn(business, 'deleteRole') -const businessListRolesMock = vi.spyOn(business, 'listRoles') -const businessPatchRolesMock = vi.spyOn(business, 'patchRoles') -const businessCountRolesMembersMock = vi.spyOn(business, 'countRolesMembers') - -describe('tests projectRoleContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const projectId = faker.string.uuid() - const roleId = faker.string.uuid() - - describe('listProjectRoles', () => { - it('should return roles for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessListRolesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectRoleContract.listProjectRoles.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectRoleContract.listProjectRoles.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - }) - - describe('createProjectRole', () => { - it('should create role for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(201) - }) - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectRoleContract.createProjectRole.path.replace(':projectId', projectId)) - .body({ name: 'nouveau rôle' }) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) - - describe('patchProjectRoles', () => { - it('should patch roles for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessPatchRolesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessPatchRolesMock.mockResolvedValue(new BadRequest400('une erreur')) - const response = await app.inject() - .patch(projectRoleContract.patchProjectRoles.path.replace(':projectId', projectId)) - .body([{ id: roleId, name: 'nouveau rôle' }]) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('projectRoleMemberCounts', () => { - it('should return member counts for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCountRolesMembersMock.mockResolvedValueOnce({}) - - const response = await app.inject() - .get(projectRoleContract.projectRoleMemberCounts.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual({}) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectRoleContract.projectRoleMemberCounts.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - }) - - describe('deleteProjectRole', () => { - it('should delete role for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteRoleMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 for locked project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not permited', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_ROLES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateRoleMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .delete(projectRoleContract.deleteProjectRole.path.replace(':projectId', projectId).replace(':roleId', roleId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts deleted file mode 100644 index 2773fabe9..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-role/router.ts +++ /dev/null @@ -1,90 +0,0 @@ -// import { AdminAuthorized, ProjectAuthorized, projectRoleContract } from '@cpn-console/shared' -// import { - // countRolesMembers, - // createRole, - // deleteRole, - // listRoles, - // patchRoles, -// } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403, NotFound404 } from '@old-server/utils/errors' - -// export function projectRoleRouter() { - // return serverInstance.router(projectRoleContract, { - // // Récupérer des projets - // listProjectRoles: async ({ request: req, params }) => { - // const { projectId } = params - // const perms = await authUser(req, { id: projectId }) - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - // const body = await listRoles(projectId) - - // return { - // status: 200, - // body, - // } - // }, - - // createProjectRole: async ({ request: req, params: { projectId }, body }) => { - // const perms = await authUser(req, { id: projectId }) - - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - // if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const resBody = await createRole(projectId, body) - - // return { - // status: 201, - // body: resBody, - // } - // }, - - // patchProjectRoles: async ({ request: req, params: { projectId }, body }) => { - // const perms = await authUser(req, { id: projectId }) - - // if (!perms.projectPermissions) return new NotFound404() - // if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const resBody = await patchRoles(projectId, body) - // if (resBody instanceof ErrorResType) return resBody - - // return { - // status: 200, - // body: resBody, - // } - // }, - - // projectRoleMemberCounts: async ({ request: req, params }) => { - // const { projectId } = params - // const perms = await authUser(req, { id: projectId }) - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - - // const resBody = await countRolesMembers(projectId) - - // return { - // status: 200, - // body: resBody, - // } - // }, - - // deleteProjectRole: async ({ request: req, params: { projectId, roleId } }) => { - // const perms = await authUser(req, { id: projectId }) - // if (!perms.projectPermissions) return new NotFound404() - // if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const resBody = await deleteRole(roleId) - - // return { - // status: 204, - // body: resBody, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts deleted file mode 100644 index 531e3ba12..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/business.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Project, ProjectPlugin } from '@prisma/client' -import type { - PermissionTarget, - PluginsUpdateBody, - ServiceUrl, -} from '@cpn-console/shared' -import { editStrippers, populatePluginManifests, servicesInfos } from '@cpn-console/hooks' -import type { ZoneObject } from '@cpn-console/hooks' -import { - getAdminPlugin, - getProjectInfosByIdOrThrow, - getProjectStore, - getPublicClusters, - saveProjectStore, -} from '@old-server/resources/queries-index' - -export type ConfigRecords = { - key: string - pluginName: string - value: string | number | null -}[] - -export function dbToObj(records: Omit[]): PluginsUpdateBody { - const obj: PluginsUpdateBody = {} - for (const record of records) { - if (!obj[record.pluginName]) obj[record.pluginName] = {} - obj[record.pluginName][record.key] = record.value - } - return obj -} - -export function objToDb(obj: PluginsUpdateBody): ConfigRecords { - return Object.entries(obj) - .map(([pluginName, values]) => Object.entries(values) - .map(([key, value]) => ({ pluginName, key, value }))) - .flat() -} - -export async function getProjectServices(projectId: Project['id'], permissionTarget: PermissionTarget) { - // Pré-requis - const project = await getProjectInfosByIdOrThrow(projectId) - - const [projectStore, globalConfig] = await Promise.all([ - getProjectStore(projectId), - getAdminPlugin(), - ]) - const store = dbToObj([...projectStore, ...globalConfig]) - - const publicClusters = await getPublicClusters() - project.clusters = project.clusters.concat(publicClusters) - const zones: Map = new Map() // Pour dédoublonnage des zones - project.clusters.map(c => zones.set(c.zone.id, c.zone)) - - return Object.values(servicesInfos).map(({ name, title, to, imgSrc, description }) => { - let urls: ServiceUrl[] = [] - const toResponse = to - ? to({ - clusters: project.clusters, - zones: Array.from(zones.values()), - environments: project.environments, - project, - store, - }) - : [] - if (Array.isArray(toResponse)) { - urls = toResponse.map(res => ({ name: res.title ?? '', description: res.description ?? '', to: res.to })) - } else if (typeof toResponse === 'string') { - urls = [{ to: toResponse, name: '' }] - } else if (toResponse) { - urls = [{ name: toResponse.title ?? '', to: toResponse.to }] - } - const manifest = populatePluginManifests({ - data: { - project: projectStore, - global: globalConfig, - }, - permissionTarget, - pluginName: name, - select: { - global: true, - project: true, - }, - }) - return { imgSrc, title, name, urls, manifest, description } - }).filter(s => s.urls.length || s.manifest.global?.length || s.manifest.project?.length) -} - -export async function updateProjectServices(projectId: Project['id'], data: PluginsUpdateBody, stripperRoles: Array<'user' | 'admin'>) { - for (const role of stripperRoles) { - const parsedData = editStrippers.project[role].safeParse(data) - if (!parsedData.success) continue - await saveProjectStore(objToDb(parsedData.data), projectId) - } - return null -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts deleted file mode 100644 index a966eb59a..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/queries.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Project } from '@prisma/client' -import type { ConfigRecords } from './business' -import prisma from '@old-server/prisma' - -// CONFIG -export function getProjectStore(projectId: Project['id']) { - return prisma.projectPlugin.findMany({ - where: { projectId }, - select: { - key: true, - pluginName: true, - value: true, - }, - }) -} - -export const getAdminPlugin = prisma.adminPlugin.findMany - -export async function saveProjectStore(records: ConfigRecords, projectId: Project['id']) { - for (const { pluginName, key, value } of records) { - if (value === null) { - await prisma.projectPlugin.delete({ - where: { - projectId_pluginName_key: { - projectId, - pluginName, - key, - }, - }, - }) - } else { - await prisma.projectPlugin.upsert({ - create: { - pluginName, - projectId, - key, - value: value.toString(), - }, - update: { - key, - value: value.toString(), - pluginName, - }, - where: { - projectId_pluginName_key: { - projectId, - pluginName, - key, - }, - }, - }) - } - } -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts deleted file mode 100644 index 2e1972238..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { PROJECT_PERMS, projectServiceContract } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetServicesMock = vi.spyOn(business, 'getProjectServices') -const businessUpdateServicesMock = vi.spyOn(business, 'updateProjectServices') - -describe('projectServiceRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const projectId = faker.string.uuid() - - describe('getServices', () => { - it('should return services for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetServicesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) - .query({ permissionTarget: 'user' }) - .end() - - expect(businessGetServicesMock).toHaveBeenCalledWith(projectId, 'user') - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should not return admin services for non admin', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetServicesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) - .query({ permissionTarget: 'admin' }) - .end() - - expect(businessGetServicesMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - - it('should return services for admin', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.GUEST }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetServicesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) - .end() - - expect(businessGetServicesMock).toHaveBeenCalledWith(projectId, 'user') - expect(response.statusCode).toEqual(200) - expect(response.json()).toEqual([]) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectServiceContract.getServices.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - }) - - describe('updateProjectServices', () => { - const updateData = { serviceA: { param1: 'value' } } - - it('should update services for project manager', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateServicesMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(businessUpdateServicesMock).toHaveBeenCalledWith(projectId, updateData, ['user']) - expect(response.statusCode).toEqual(204) - }) - - it('should update services for project admin', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateServicesMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(businessUpdateServicesMock).toHaveBeenCalledWith(projectId, updateData, ['user', 'admin']) - expect(response.statusCode).toEqual(204) - }) - - it('should return 404 for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(projectServiceContract.updateProjectServices.path.replace(':projectId', projectId)) - .body(updateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts deleted file mode 100644 index 2fa4a7eb2..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project-service/router.ts +++ /dev/null @@ -1,38 +0,0 @@ -// import { AdminAuthorized, ProjectAuthorized, projectServiceContract } from '@cpn-console/shared' -// import { getProjectServices, updateProjectServices } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { Forbidden403, NotFound404 } from '@old-server/utils/errors' - -// export function projectServiceRouter() { - // return serverInstance.router(projectServiceContract, { - // // Récupérer les services d'un projet - // getServices: async ({ request: req, params: { projectId }, query }) => { - // const perms = await authUser(req, { id: projectId }) - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - // if (!AdminAuthorized.isAdmin(perms.adminPermissions) && query.permissionTarget === 'admin') return new Forbidden403('Vous ne pouvez pas demander les paramètres admin') - - // const body = await getProjectServices(projectId, query.permissionTarget) - - // return { - // status: 200, - // body, - // } - // }, - - // updateProjectServices: async ({ request: req, params: { projectId }, body }) => { - // const perms = await authUser(req, { id: projectId }) - // if (!ProjectAuthorized.Manage(perms)) return new NotFound404() - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - - // const allowedRoles: Array<'user' | 'admin'> = AdminAuthorized.isAdmin(perms.adminPermissions) ? ['user', 'admin'] : ['user'] - - // const resBody = await updateProjectServices(projectId, body, allowedRoles) - // return { - // status: 204, - // body: resBody, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts deleted file mode 100644 index 10195c221..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.spec.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Cluster, Project, ProjectMembers, ProjectRole, User } from '@prisma/client' -import prisma from '../../__mocks__/prisma' -import { hook } from '../../__mocks__/utils/hook-wrapper' -import { dbToObj } from '../project-service/business' -import * as userBusiness from '../user/business' -import { - BadRequest400, - ErrorResType, - Unprocessable422, -} from '../../utils/errors' -import { archiveProject, chunk, createProject, generateProjectsData, generateSlug, getProjectSecrets, listProjects, replayHooks, updateProject } from './business' - -vi.mock('../../utils/hook-wrapper', async () => ({ - hook, -})) - -const logViaSessionMock = vi.spyOn(userBusiness, 'logViaSession') - -const projectId = faker.string.uuid() - -const user: User = { - id: faker.string.uuid(), - createdAt: new Date(), - updatedAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - adminRoleIds: [], - type: 'human', - lastLogin: null, -} -const project: Project & { - clusters: Pick[] - members: ProjectMembers[] - roles: ProjectRole[] - owner: User -} = { - createdAt: new Date(), - updatedAt: new Date(), - description: '', - everyonePerms: 649n, - id: faker.string.uuid(), - locked: false, - name: faker.string.alphanumeric(8), - status: 'created', - ownerId: faker.string.uuid(), - clusters: [], - roles: [], - members: [], -} -const reqId = faker.string.uuid() -describe('test project business utils', () => { - it('should transform arrow ', async () => { - const result = dbToObj([{ key: 'test', pluginName: 'test', value: 'test' }]) - expect(result).toEqual({ test: { test: 'test' } }) - }) -}) - -describe('test project business logic', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - describe('listProjects', () => { - it('should return stringified perms', async () => { - prisma.project.findMany.mockResolvedValue([{ everyonePerms: 5n, clusters: [], roles: [{ permissions: 28n }] }]) - const response = await listProjects({}, user.id) - expect(response[0].everyonePerms).toBe('5') - expect(response[0].roles[0].permissions).toBe('28') - }) - }) - describe('getProjectSecrets', () => { - const getResultsHook = { - failed: false, - args: {}, - results: { - registry: { - secrets: { - token: 'myToken', - }, - status: { - failed: false, - }, - }, - }, - } - it('should return transform secret', async () => { - hook.project.getSecrets.mockResolvedValue(getResultsHook) - - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) - const response = await getProjectSecrets(projectId) - - // according to src/utils/mocks.ts - expect(JSON.stringify(response)).toContain('myToken') - }) - - it('should return projects secrets', async () => { - hook.project.getSecrets.mockResolvedValue(getResultsHook) - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) - prisma.project.findMany.mockResolvedValue({ id: projectId }) - const response = await getProjectSecrets(projectId) - // according to src/utils/mocks.ts - expect(JSON.stringify(response)).toContain('myToken') - }) - - it('should return hook error', async () => { - hook.project.getSecrets.mockResolvedValue({ failed: true }) - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId }) - prisma.project.findMany.mockResolvedValue({ id: projectId }) - const response = await getProjectSecrets(projectId) - // according to src/utils/mocks.ts - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - - describe('createProject', () => { - it('should create project', async () => { - logViaSessionMock.mockResolvedValue({ user }) - - prisma.project.create.mockResolvedValue({ ...project, status: 'initializing' }) - prisma.project.findFirst.mockResolvedValue(undefined) - prisma.project.findMany.mockResolvedValue([]) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const projectRes = await createProject(project, user, reqId) - - expect(projectRes.name).toEqual(project.name) - expect(prisma.project.create).toHaveBeenCalledTimes(1) - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - }) - - it('should return plugins failed', async () => { - logViaSessionMock.mockResolvedValue({ user }) - - prisma.project.create.mockResolvedValue({ ...project, status: 'initializing' }) - prisma.project.findFirst.mockResolvedValue(undefined) - prisma.project.findMany.mockResolvedValue([]) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const response = await createProject(project, user, reqId) - - expect(prisma.project.create).toHaveBeenCalledTimes(1) - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - describe('updateProject', () => { - const updatedProjet = { - description: faker.lorem.lines(2), - everyonePerms: '5', - } - const reqId = faker.string.uuid() - const members: ProjectMembers[] = [{ userId: faker.string.uuid(), projectId: project.id, roleIds: [], user: { type: 'human' } }] - it('should update project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members }) - prisma.project.update.mockResolvedValue(project) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - await updateProject({ ...updatedProjet, ownerId: members[0].userId }, project.id, user, reqId) - - expect(prisma.project.update).toHaveBeenCalledTimes(2) - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - }) - - it('should update nothing', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members }) - prisma.project.update.mockResolvedValue(project) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - await updateProject({ }, project.id, user, reqId) - - expect(prisma.project.update).toHaveBeenCalledTimes(0) - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - }) - - it('should not update if project archived', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, status: 'archived' }) - prisma.project.update.mockResolvedValue(project) - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - - const response = await updateProject({ }, project.id, user, reqId) - - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.project.update).toHaveBeenCalledTimes(0) - expect(prisma.log.create).toHaveBeenCalledTimes(0) - expect(hook.project.upsert).toHaveBeenCalledTimes(0) - }) - - it('should not update project, cause missing member', async () => { - hook.project.upsert.mockResolvedValue({ results: {}, project: { ...project } }) - logViaSessionMock.mockResolvedValue({ user }) - - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, members: [] }) - - const response = await updateProject({ ownerId: members[0].userId }, project.id, user, reqId) - - expect(prisma.project.findUniqueOrThrow).toHaveBeenCalledTimes(1) - expect(response).toBeInstanceOf(BadRequest400) - expect(hook.project.upsert).toHaveBeenCalledTimes(0) - expect(prisma.log.update).toHaveBeenCalledTimes(0) - }) - - it('should return plugins failed', async () => { - logViaSessionMock.mockResolvedValue({ user }) - - prisma.project.findUniqueOrThrow.mockResolvedValue({ status: 'created' }) - hook.project.upsert.mockResolvedValue({ results: { failed: true }, project: { ...project } }) - - const response = await updateProject(updatedProjet, project.id, user, reqId) - - expect(prisma.project.update).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - describe('replayHooks', () => { - const reqId = faker.string.uuid() - - it('should replay hooks', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'created' }) - hook.project.upsert.mockResolvedValue({ results: { failed: false } }) - - await replayHooks(project.id, user, reqId) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - }) - - it('should not replay hooks on archived project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'archived' }) - hook.project.upsert.mockResolvedValue({ results: { failed: false } }) - - const response = await replayHooks(project.id, user, reqId) - - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.log.create).toHaveBeenCalledTimes(0) - expect(hook.project.upsert).toHaveBeenCalledTimes(0) - }) - - it('should not replay hooks on locked project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: true, status: 'created' }) - hook.project.upsert.mockResolvedValue({ results: { failed: false } }) - - const response = await replayHooks(project.id, user, reqId) - - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.log.create).toHaveBeenCalledTimes(0) - expect(hook.project.upsert).toHaveBeenCalledTimes(0) - }) - - it('should update nothing and return error', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ locked: false, status: 'created' }) - hook.project.upsert.mockResolvedValue({ results: { failed: true } }) - - const response = await replayHooks(project.id, user, reqId) - - expect(prisma.log.create).toHaveBeenCalledTimes(1) - expect(hook.project.upsert).toHaveBeenCalledTimes(1) - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - - describe('archiveProject', () => { - it('should archive project', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false }) - hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) - const response = await archiveProject(project.id, user, reqId) - expect(response).toBeNull() - expect(prisma.project.update).toHaveBeenLastCalledWith({ - where: { id: project.id }, - data: { - clusters: { set: [] }, - }, - }) - }) - - it('should not archive a project already archived', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false, status: 'archived' }) - hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) - const response = await archiveProject(project.id, user, reqId) - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.project.update).toHaveBeenCalledTimes(0) - }) - - it('should not archive a project locked', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: true, status: 'created' }) - hook.project.delete.mockResolvedValue({ results: { failed: false }, project: Promise.resolve({ status: 'archived' }) }) - const response = await archiveProject(project.id, user, reqId) - expect(response).toBeInstanceOf(ErrorResType) - expect(prisma.project.update).toHaveBeenCalledTimes(0) - }) - - it('should return hook fail', async () => { - prisma.project.findUniqueOrThrow.mockResolvedValue({ id: projectId, locked: false }) - hook.project.delete.mockResolvedValue({ results: { failed: true }, project: Promise.resolve({ status: 'failed' }) }) - const response = await archiveProject(project.id, user, reqId) - expect(response).toBeInstanceOf(Unprocessable422) - }) - }) - - describe('generateProjectsData', () => { - it('shoud return string, very bad test ...', async () => { - prisma.project.findMany.mockResolvedValue([{ name: 'test' }]) - const response = await generateProjectsData() - expect(response).toBeTypeOf('string') - }) - }) -}) - -describe('chunk function', () => { - it('should return 5 elements', () => { - const letters = ['A', 'B', 'C', 'D', 'E'] - expect(chunk(letters, 5)).toEqual([letters]) - }) - it('should return 3,2 elements', () => { - const letters = ['A', 'B', 'C', 'D', 'E'] - expect(chunk(letters, 3)).toEqual([['A', 'B', 'C'], ['D', 'E']]) - }) - it('should return 4 elements', () => { - const letters = ['A', 'B', 'C', 'D'] - expect(chunk(letters, 5)).toEqual([letters]) - }) -}) - -describe('generateSlug', () => { - it('should return prefix, no array', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix) - expect(generated).toEqual(prefix) - }) - it('should return prefix, empty array', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, []) - expect(generated).toEqual(prefix) - }) - it('should return prefix, no match', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, [faker.string.alphanumeric(5), faker.string.alphanumeric(5)]) - expect(generated).toEqual(prefix) - }) - it('should return generated slug at 1 or 0, all matchs', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, [prefix]) - expect(generated).match(/-[01]$/) - }) - it('should return generated slug at 4, all matchs', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, [prefix, `${prefix}-0`, `${prefix}-1`, `${prefix}-2`, `${prefix}-3`]) - expect(generated).match(/-4$/) - }) - it('should fill empty space', () => { - const prefix = faker.string.alphanumeric(5) - const generated = generateSlug(prefix, [prefix, `${prefix}-0`, `${prefix}-1`, `${prefix}-3`]) - expect(generated).match(/-2$/) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts deleted file mode 100644 index 915000520..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/business.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { servicesInfos } from '@cpn-console/hooks'; -import type { projectContract } from '@cpn-console/shared'; -import { ProjectStatusSchema } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; -import { - addLogs, - deleteAllEnvironmentForProject, - deleteAllRepositoryForProject, - getAllProjectsDataForExport, - getProjectOrThrow, - getSlugs, - initializeProject, - listProjects as listProjectsQuery, - lockProject, - updateProject as updateProjectQuery, -} from '@old-server/resources/queries-index'; -import type { UserDetails } from '@old-server/types/index'; -import { whereBuilder } from '@old-server/utils/controller'; -import type { ErrorResType } from '@old-server/utils/errors'; -import { - BadRequest400, - Forbidden403, - Unprocessable422, -} from '@old-server/utils/errors'; -import { hook } from '@old-server/utils/hook-wrapper'; -import type { Project, User } from '@prisma/client'; -import { json2csv } from 'json-2-csv'; - -// server tuning -const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT - ? Number(process.env.PARALLEL_BULK_LIMIT) - : 5; - -export function generateSlug(prefix: string, existingSlugs?: string[]) { - if (!existingSlugs?.includes(prefix)) { - return prefix; - } - let idx = 1; - let generated = `${prefix}-${idx}`; - while (existingSlugs.includes(generated)) { - idx++; - generated = `${prefix}-${idx}`; - } - return generated; -} - -const projectStatus = ProjectStatusSchema._def.values; -export async function listProjects( - { - status, - statusIn, - statusNotIn, - filter = 'member', - ...query - }: typeof projectContract.listProjects.query._type, - userId: User['id'] | undefined, -) { - return listProjectsQuery({ - ...query, - status: whereBuilder({ - enumValues: projectStatus, - eqValue: status, - inValues: statusIn, - notInValues: statusNotIn, - }), - filter, - userId, - }).then((projects) => - projects.map(({ clusters, ...project }) => ({ - ...project, - clusterIds: clusters.map(({ id }) => id), - roles: project.roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - everyonePerms: project.everyonePerms.toString(), - })), - ); -} - -export async function getProjectSecrets(projectId: string) { - const hookReply = await hook.project.getSecrets(projectId); - if (hookReply.failed) { - return new Unprocessable422( - 'Echec des services à la récupération des secrets du projet', - ); - } - - return Object.fromEntries( - Object.entries(hookReply.results) - // @ts-ignore - .filter(([_key, value]) => Object.keys(value.secrets).length) - // @ts-ignore - .map(([key, value]) => [servicesInfos[key]?.title, value.secrets]), - ); -} - -export async function createProject( - dataDto: typeof projectContract.createProject.body._type, - requestor: UserDetails, - requestId: string, -) { - if (requestor.type !== 'human') - return new BadRequest400( - 'Seuls les comptes humains peuvent créer des projets', - ); - - let slug = dataDto.name; - const projectsWithSamePrefix = await getSlugs(slug); - slug = generateSlug( - slug, - projectsWithSamePrefix?.map((project) => project.slug), - ); - - // Actions - const project = await initializeProject({ - ...dataDto, - slug, - ownerId: requestor.id, - }); - - const { results, project: projectInfos } = await hook.project.upsert( - project.id, - ); - await addLogs({ - action: 'Create Project', - data: results, - userId: requestor.id, - requestId, - projectId: project.id, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services à la création du projet', - ); - } - - return { - ...projectInfos, - clusterIds: projectInfos.clusters.map(({ id }) => id), - everyonePerms: projectInfos.everyonePerms.toString(), - roles: projectInfos.roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - }; -} - -export async function getProject(projectId: Project['id']) { - return getProjectOrThrow(projectId).then(({ clusters, ...project }) => ({ - ...project, - clusterIds: clusters.map(({ id }) => id), - roles: project.roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - everyonePerms: project.everyonePerms.toString(), - })); -} - -export async function updateProject( - { - description, - ownerId: ownerIdCandidate, - everyonePerms, - locked, - ...data - }: typeof projectContract.updateProject.body._type, - projectId: Project['id'], - requestor: UserDetails, - requestId: string, -) { - // Actions - const projectDb = await prisma.project.findUniqueOrThrow({ - where: { id: projectId }, - include: { members: { include: { user: true } } }, - }); - - if (projectDb.status === 'archived') - return new Forbidden403('Le projet est archivé'); - - if (ownerIdCandidate && ownerIdCandidate !== projectDb.ownerId) { - const memberCandidate = projectDb.members.find( - (member) => member.userId === ownerIdCandidate, - ); - if (!memberCandidate) { - return new BadRequest400( - 'Le nouveau propriétaire doit faire partie des membres actuels du projet', - ); - } - if (memberCandidate.user.type !== 'human') - return new BadRequest400( - 'Seuls les comptes humains peuvent être propriétaire de projets', - ); - if ( - !projectDb.members.find( - (member) => member.userId === projectDb.ownerId, - ) - ) { - await prisma.projectMembers.create({ - data: { userId: projectDb.ownerId, projectId }, - }); - } - await prisma.$transaction([ - prisma.projectMembers.delete({ - where: { - projectId_userId: { userId: ownerIdCandidate, projectId }, - }, - }), - prisma.project.update({ - where: { id: projectId }, - data: { ownerId: ownerIdCandidate }, - }), - ]); - } - - if ( - typeof description !== 'undefined' || - typeof everyonePerms !== 'undefined' || - typeof locked !== 'undefined' - ) { - await updateProjectQuery(projectId, { - description, - locked, - ...(everyonePerms && { everyonePerms: BigInt(everyonePerms) }), - ...data, - }); - } - - const { results, project: projectInfos } = - await hook.project.upsert(projectId); - await addLogs({ - action: 'Update Project', - data: results, - userId: requestor.id, - requestId, - projectId: projectInfos.id, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services à la mise à jour du projet', - ); - } - - return { - ...projectInfos, - clusterIds: projectInfos.clusters.map(({ id }) => id), - everyonePerms: projectInfos.everyonePerms.toString(), - roles: projectInfos.roles.map((role) => ({ - ...role, - permissions: role.permissions.toString(), - })), - }; -} - -interface ReplayHooksArgs { - projectId: Project['id']; - userId?: User['id']; - requestId: string; -} -export async function replayHooks({ - projectId, - userId, - requestId, -}: ReplayHooksArgs): Promise { - const projectDb = await prisma.project.findUniqueOrThrow({ - where: { id: projectId }, - include: { members: { include: { user: true } } }, - }); - if (projectDb.locked) return new Forbidden403('Le projet est verrouillé'); - if (projectDb.status === 'archived') - return new Forbidden403('Le projet est archivé'); - // Actions - const { results } = await hook.project.upsert(projectId); - await addLogs({ - action: 'Replay hooks for Project', - data: results, - userId, - requestId, - projectId, - }); - if (results.failed) { - return new Unprocessable422( - 'Echec des services au reprovisionnement du projet', - ); - } - return null; -} - -export async function archiveProject( - projectId: Project['id'], - requestor: UserDetails, - requestId: string, -): Promise { - // Actions - // Empty the project first - const [projectDb, ..._] = await Promise.all([ - // get initial project state - prisma.project.findUniqueOrThrow({ where: { id: projectId } }), - deleteAllRepositoryForProject(projectId), - deleteAllEnvironmentForProject(projectId), - ]); - - if (projectDb.locked) return new Forbidden403('Le projet est verrouillé'); - if (projectDb.status === 'archived') - return new BadRequest400('Le projet est archivé'); - if (projectDb.locked) { - await lockProject(projectId); - } - - // -- début - Suppression projet -- - const { results, project } = await hook.project.delete(projectId); - await addLogs({ - action: 'Delete all project resources', - data: results, - userId: requestor.id, - requestId, - projectId, - }); - if (project.status !== 'archived' && !projectDb.locked) { - await prisma.project.update({ - where: { id: projectId }, - data: { locked: false }, - }); - } - if (results.failed) { - return new Unprocessable422( - 'Echec des services à la suppression du projet', - ); - } - - // Retrait clusters -- - await prisma.project.update({ - where: { id: projectId }, - data: { - clusters: { set: [] }, - }, - }); - - // -- fin - Suppression projet -- - return null; -} - -export async function generateProjectsData() { - const projects = await getAllProjectsDataForExport(); - - return json2csv(projects, { - emptyFieldValue: '', - }); -} - -export async function bulkActionProject( - data: typeof projectContract.bulkActionProject.body._type, - requestor: UserDetails, - requestId: string, -) { - if (data.projectIds === 'all') { - data.projectIds = ( - await prisma.project.findMany({ - select: { id: true }, - where: { status: { not: 'archived' } }, - }) - ).map(({ id }) => id); - } - bulkExector( - data.projectIds.map((projectId) => { - if (data.action === 'archive') { - return () => archiveProject(projectId, requestor, requestId); - } - if (data.action === 'lock') { - return () => - updateProject( - { locked: true }, - projectId, - requestor, - requestId, - ); - } - if (data.action === 'unlock') { - return () => - updateProject( - { locked: false }, - projectId, - requestor, - requestId, - ); - } - if (data.action === 'replay') { - return () => - replayHooks({ projectId, userId: requestor.id, requestId }); - } - // should never been called - return async () => {}; - }), - ); -} - -export function chunk(arr: T[], size: number): T[][] { - return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => - arr.slice(i * size, i * size + size), - ); -} - -async function bulkExector(toExecute: Array<() => Promise>) { - const toExecuteChunked = chunk(toExecute, parallelBulkLimit); - for (const chunkToExecute of toExecuteChunked) { - await Promise.allSettled(chunkToExecute.map((fn) => fn())); - } -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts deleted file mode 100644 index dc4ff028b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/queries.ts +++ /dev/null @@ -1,371 +0,0 @@ -import type { XOR, projectContract } from '@cpn-console/shared'; -import prisma from '@old-server/prisma'; -import { uuid } from '@old-server/utils/queries-tools'; -import type { Prisma, Project, User } from '@prisma/client'; -import { ProjectStatus } from '@prisma/client'; - -// TODO: Convert to NestJS class and inject ConfigurationService to retrieve `appVersion` -const appVersion = - process.env.NODE_ENV === 'production' - ? (process.env.APP_VERSION ?? 'unknown') - : 'dev'; - -type ProjectUpdate = Partial< - Pick ->; -export function updateProject(id: Project['id'], data: ProjectUpdate) { - return prisma.project.update({ - where: { id }, - data, - include: { members: true }, - }); -} - -// SELECT -type FilterWhere = XOR< - { - userId?: User['id']; - filter: 'all'; - }, - { - userId: User['id'] | undefined; - filter: 'owned' | 'member'; - } ->; -type ListProjectWhere = Omit< - typeof projectContract.listProjects.query._type, - 'status_in' | 'status_not_in' | 'status' -> & - Pick & - FilterWhere; -export async function listProjects({ - description, - locked, - name, - status, - id, - filter, - userId, - search, - lastSuccessProvisionningVersion, -}: ListProjectWhere) { - const whereAnd: Prisma.ProjectWhereInput[] = []; - if (id) whereAnd.push({ id }); - if (locked != null) whereAnd.push({ locked }); - if (name) whereAnd.push({ name }); - if (status) whereAnd.push({ status }); - if (description) whereAnd.push({ description: { contains: description } }); - if (lastSuccessProvisionningVersion) { - if (lastSuccessProvisionningVersion === 'outdated') - whereAnd.push({ - lastSuccessProvisionningVersion: { not: appVersion }, - }); - else if (lastSuccessProvisionningVersion === 'last') - whereAnd.push({ - lastSuccessProvisionningVersion: { equals: appVersion }, - }); - else whereAnd.push({ lastSuccessProvisionningVersion }); - } - if (search) { - whereAnd.push({ - OR: [ - { - name: { contains: search }, - }, - { - owner: { email: { contains: search } }, - }, - ], - }); - } - - if (filter === 'owned') { - whereAnd.push({ ownerId: userId }); - } else if (filter === 'member') { - whereAnd.push({ - OR: [ - { - members: { some: { userId } }, - }, - { - ownerId: userId, - }, - ], - }); - } - - return prisma.project.findMany({ - where: { AND: whereAnd }, - include: { - clusters: { select: { id: true } }, - members: { include: { user: true } }, - roles: true, - owner: true, - }, - }); -} - -export function getProjectOrThrow(id: Project['id'] | Project['slug']) { - return prisma.project.findFirstOrThrow({ - where: uuid.test(id) ? { id } : { slug: id }, - include: { - clusters: { select: { id: true } }, - members: { include: { user: true } }, - roles: true, - owner: true, - }, - }); -} - -export function getProjectInfosByIdOrThrow(projectId: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { - id: projectId, - }, - include: { - environments: true, - clusters: { include: { zone: true } }, - }, - }); -} - -export function getProjectMembers(projectId: Project['id']) { - return prisma.projectMembers.findMany({ - where: { - projectId, - }, - include: { user: true }, - }); -} - -export function getProjectById(id: Project['id']) { - return prisma.project.findUnique({ where: { id } }); -} - -export const baseProjectIncludes = { - members: { include: { user: true } }, - clusters: true, - roles: true, - owner: true, -} as const; - -export function getProjectInfos(id: Project['id']) { - return prisma.project.findUnique({ - where: { id }, - include: baseProjectIncludes, - }); -} - -export function getProjectInfosOrThrow(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: baseProjectIncludes, - }); -} - -export function getProjectInfosAndRepos(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: { - ...baseProjectIncludes, - repositories: true, - }, - }); -} - -export function getSlugs(slugPrefix: string) { - return prisma.project.findMany({ - where: { - slug: { startsWith: slugPrefix }, - }, - }); -} - -export function getAllProjectsDataForExport() { - return prisma.project.findMany({ - select: { - name: true, - description: true, - createdAt: true, - updatedAt: true, - environments: { - select: { - name: true, - stage: true, - cluster: { - select: { label: true }, - }, - }, - }, - owner: true, - }, - }); -} - -export function getRolesByProjectId(projectId: Project['id']) { - return prisma.projectRole.findMany({ - where: { projectId }, - }); -} - -const clusterInfosSelect = { - id: true, - infos: true, - label: true, - external: true, - privacy: true, - secretName: true, - kubeconfig: true, - clusterResources: true, - cpu: true, - gpu: true, - memory: true, - zone: { - select: { - id: true, - slug: true, - argocdUrl: true, - label: true, - }, - }, -}; -export function getHookProjectInfos(id: Project['id']) { - return prisma.project.findUniqueOrThrow({ - where: { id }, - include: { - members: { - include: { user: true }, - where: { user: { type: 'human' } }, - }, - clusters: { select: clusterInfosSelect }, - environments: { - include: { - stage: true, - cluster: { - select: clusterInfosSelect, - }, - }, - }, - repositories: true, - plugins: { - select: { - key: true, - pluginName: true, - value: true, - }, - }, - owner: true, - roles: true, - }, - }); -} - -// CREATE -interface CreateProjectParams { - name: Project['name']; - description?: Project['description']; - ownerId: User['id']; - slug: Project['slug']; - limitless: boolean; - hprodCpu: number; - hprodGpu: number; - hprodMemory: number; - prodCpu: number; - prodGpu: number; - prodMemory: number; -} - -export function initializeProject(params: CreateProjectParams) { - return prisma.project.create({ - data: { - description: params.description ?? '', - status: ProjectStatus.created, - locked: false, - ...params, - }, - }); -} - -// UPDATE -export function lockProject(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { locked: true }, - }); -} - -export function updateProjectCreated(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { - status: ProjectStatus.created, - lastSuccessProvisionningVersion: appVersion, - }, - include: baseProjectIncludes, - }); -} - -export function updateProjectFailed(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { status: ProjectStatus.failed }, - include: baseProjectIncludes, - }); -} - -export function updateProjectWarning(id: Project['id']) { - return prisma.project.update({ - where: { id }, - data: { status: ProjectStatus.warning }, - include: baseProjectIncludes, - }); -} - -export function addUserToProject({ - project, - user, -}: { - project: Project; - user: User; -}) { - return prisma.projectMembers.create({ - data: { - userId: user.id, - projectId: project.id, - }, - }); -} - -export function removeUserFromProject({ - projectId, - userId, -}: { - projectId: Project['id']; - userId: User['id']; -}) { - return prisma.projectMembers.delete({ - where: { - projectId_userId: { - projectId, - userId, - }, - }, - }); -} - -export async function archiveProject(id: Project['id']) { - const project = await prisma.project.findUnique({ - where: { id }, - select: { name: true, slug: true }, - }); - return prisma.project.update({ - where: { id }, - data: { - name: `${project?.name}_${Date.now()}_archived`, - slug: `${project?.slug}_${Date.now()}_archived`, - status: ProjectStatus.archived, - locked: true, - }, - include: baseProjectIncludes, - }); -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts deleted file mode 100644 index cb1362cbe..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.spec.ts +++ /dev/null @@ -1,440 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ProjectV2 } from '@cpn-console/shared' -import { PROJECT_PERMS, projectContract } from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getProjectMockInfos, getRandomRequestor, getUserMockInfos } from '../../utils/mocks' -import { BadRequest400 } from '../../utils/errors' -import * as business from './business' -import type { UserDetails } from '../../types/index' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMock = vi.spyOn(business, 'listProjects') -const businessCreateMock = vi.spyOn(business, 'createProject') -const businessUpdateMock = vi.spyOn(business, 'updateProject') -const businessDeleteMock = vi.spyOn(business, 'archiveProject') -const businessSyncMock = vi.spyOn(business, 'replayHooks') -const bulkActionProjectMock = vi.spyOn(business, 'bulkActionProject') -const businessGetSecretsMock = vi.spyOn(business, 'getProjectSecrets') -const businessGenerateDataMock = vi.spyOn(business, 'generateProjectsData') - -describe('test projectContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - const projectOwner: ProjectV2['owner'] = { - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - id: faker.string.uuid(), - type: 'human', - } - const projectId = faker.string.uuid() - const project: Omit = { - name: faker.string.alpha({ length: 10, casing: 'lower' }), - slug: faker.string.alpha({ length: 5, casing: 'lower' }), - description: faker.string.alpha({ length: 5 }), - limitless: false, - hprodCpu: faker.number.int({ min: 0, max: 1000 }), - hprodGpu: faker.number.int({ min: 0, max: 1000 }), - hprodMemory: faker.number.int({ min: 0, max: 1000 }), - prodCpu: faker.number.int({ min: 0, max: 1000 }), - prodGpu: faker.number.int({ min: 0, max: 1000 }), - prodMemory: faker.number.int({ min: 0, max: 1000 }), - clusterIds: [], - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - locked: false, - status: 'created', - everyonePerms: '0', - members: [], - owner: projectOwner, - ownerId: projectOwner.id, - roles: [], - lastSuccessProvisionningVersion: null, - } - describe('check unauthorized user on project behaviour', () => { - // UPDATE - it('on Update', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(project) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(404) - expect(response.json()).toEqual({ message: 'Not Found' }) - }) - - it('on Update without enough perms', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(project) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Forbidden' }) - }) - - // REPLAY - it('on replay', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) - .end() - - expect(businessSyncMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(404) - expect(response.json()).toEqual({ message: 'Not Found' }) - }) - - // SECRETS - it('on see secret', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) - .end() - - expect(businessGetSecretsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(404) - expect(response.json()).toEqual({ message: 'Not Found' }) - }) - - // ARCHIVE - it('on archive', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectContract.archiveProject.path.replace(':projectId', projectId)) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(404) - expect(response.json()).toEqual({ message: 'Not Found' }) - }) - }) - describe('listProjects', () => { - it('should return list of projects', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - const projects = [] - businessListMock.mockResolvedValueOnce(projects) - const response = await app.inject() - .get(projectContract.listProjects.path) - .end() - - expect(businessListMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(projects) - expect(response.statusCode).toEqual(200) - }) - it('should return 400 for non-admin with "all" filter', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - const response = await app.inject() - .get(`${projectContract.listProjects.path}?filter=all`) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('createProject', () => { - it('should create and return project for authorized user', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce({ id: projectId, ...project }) - const response = await app.inject() - .post(projectContract.createProject.path) - .body(project) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ id: projectId, ...project }) - expect(response.statusCode).toEqual(201) - }) - - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(projectContract.createProject.path) - .body(project) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('updateProject', () => { - const projectUpdated: Partial = { description: faker.string.alpha({ length: 5 }) } - - it('should update and return project for authorized user', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(projectUpdated) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) - expect(response.statusCode).toEqual(200) - }) - - it('should not update ownerId if not permitted', async () => { - const userDetails = getRandomRequestor() - const projectPerms = getProjectMockInfos({ projectOwnerId: faker.string.uuid(), projectPermissions: PROJECT_PERMS.MANAGE }) - const projectUpdated = { ownerId: faker.string.uuid(), description: faker.lorem.words() } - const user = getUserMockInfos(false, userDetails as UserDetails, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(projectUpdated) - .end() - - expect(businessUpdateMock).toHaveBeenCalledWith({ description: projectUpdated.description }, projectId, user.user, expect.any(String)) - expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) - expect(response.statusCode).toEqual(200) - }) - - it('should update ownerId and return project', async () => { - const requestor = getRandomRequestor() - const projectPerms = getProjectMockInfos({ projectOwnerId: requestor.id, projectPermissions: PROJECT_PERMS.MANAGE }) - const projectUpdated = { ownerId: faker.string.uuid(), description: faker.lorem.words() } - const user = getUserMockInfos(false, requestor as UserDetails, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: projectId, ...project, ...projectUpdated }) - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(projectUpdated) - .end() - - expect(businessUpdateMock).toHaveBeenCalledWith(projectUpdated, projectId, user.user, expect.any(String)) - expect(response.json()).toEqual({ id: projectId, ...project, ...projectUpdated }) - expect(response.statusCode).toEqual(200) - }) - - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(projectContract.updateProject.path.replace(':projectId', projectId)) - .body(project) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(1) - expect(response.statusCode).toEqual(400) - }) - }) - - describe('archiveProject', () => { - it('should archive project for authorized user', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(1) - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return projects data for admin', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(projectContract.archiveProject.path.replace(':projectId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getProjectSecrets', () => { - it('should return project secrets for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const secrets = {} - businessGetSecretsMock.mockResolvedValueOnce(secrets) - const response = await app.inject() - .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) - .end() - - expect(businessGetSecretsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(secrets) - expect(response.statusCode).toEqual(200) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetSecretsMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for unauthorized access to secrets', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectContract.getProjectSecrets.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('replayHooksForProject', () => { - it('should replay hooks for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(null) - const response = await app.inject() - .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) - .end() - - expect(businessSyncMock).toHaveBeenCalledTimes(1) - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for unauthorized access to replay hooks', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - const response = await app.inject() - .put(projectContract.replayHooksForProject.path.replace(':projectId', projectId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getProjectsData', () => { - it('should return projects data for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - const data = '' - businessGenerateDataMock.mockResolvedValueOnce(data) - const response = await app.inject() - .get(projectContract.getProjectsData.path) - .end() - - expect(businessGenerateDataMock).toHaveBeenCalledTimes(1) - expect(response.body).toEqual(data) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for non-admin user', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(projectContract.getProjectsData.path) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) - - describe('bulkActionProject', () => { - it('should executebulk for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE }) - const user = getUserMockInfos(true, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(null) - const response = await app.inject() - .post(projectContract.bulkActionProject.path) - .body({ action: 'lock', projectIds: [projectId] }) - .end() - - expect(response.json()).toBeNull() - expect(bulkActionProjectMock).toHaveBeenCalledTimes(1) - expect(response.statusCode).toEqual(202) - }) - - it('should return 403 for unauthorized access to bulk update', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_ENVIRONMENTS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - const response = await app.inject() - .post(projectContract.bulkActionProject.path) - .body({ action: 'lock', projectIds: [projectId] }) - .end() - - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts deleted file mode 100644 index c3f737324..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/project/router.ts +++ /dev/null @@ -1,199 +0,0 @@ -// import type { AsyncReturnType } from '@cpn-console/shared' -// import { AdminAuthorized, ProjectAuthorized, projectContract } from '@cpn-console/shared' -// import { - // archiveProject, - // bulkActionProject, - // createProject, - // generateProjectsData, - // getProject, - // getProjectSecrets, - // listProjects, - // replayHooks, - // updateProject, -// } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { BadRequest400, ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' - -// export function projectRouter() { - // return serverInstance.router(projectContract, { - - // // Récupérer des projets - // listProjects: async ({ request: req, query }) => { - // const { adminPermissions, user } = await authUser(req) - // let body: AsyncReturnType = [] - - // if (adminPermissions && !user) { // c'est donc un compte de service - // query.filter = 'all' - // } - // if (query.filter === 'all' && !AdminAuthorized.isAdmin(adminPermissions)) { - // return new BadRequest400('Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre \'all\'') - // } - - // body = await listProjects( - // query, - // user?.id, - // ) - - // return { - // status: 200, - // body, - // } - // }, - - // // Récupérer les secrets d'un projet - // getProjectSecrets: async ({ request: req, params }) => { - // const projectId = params.projectId - // const perms = await authUser(req, { id: projectId }) - // if (!perms.projectPermissions) return new NotFound404() - // if (!ProjectAuthorized.SeeSecrets(perms)) return new Forbidden403() - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const body = await getProjectSecrets(projectId) - - // if (body instanceof ErrorResType) return body - - // return { - // status: 200, - // body, - // } - // }, - - // // Créer un projet - // createProject: async ({ request: req, body: data }) => { - // const perms = await authUser(req) - // if (perms.user?.type !== 'human') return new Unauthorized401('Cannot find requestor in database') - // const body = await createProject(data, perms.user, req.id) - - // if (body instanceof ErrorResType) return body - - // return { - // status: 201, - // body, - // } - // }, - - // // Récuperer un seul projet - // getProject: async ({ request: req, params }) => { - // const projectId = params.projectId - // const perms = await authUser(req, { id: projectId }) - // const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - // if (!perms.projectId) return new NotFound404() - // if (!isAdmin) { - // if (!perms.projectPermissions) { - // return new NotFound404() - // } - // if (perms.projectStatus === 'archived') { - // return new NotFound404() - // } - // } - - // const body = await getProject(projectId) - - // return { - // status: 200, - // body, - // } - // }, - - // // Mettre à jour un projet - // updateProject: async ({ request: req, params, body: data }) => { - // const projectId = params.projectId - // const perms = await authUser(req, { id: projectId }) - - // if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - // const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - // const isOwner = perms.projectOwnerId === perms.user.id - - // if (!perms.projectPermissions && !isAdmin) return new NotFound404() - // if (!isAdmin) { // filtrage des clés par niveau de permissions - // delete data.locked - // if (!isOwner) { - // delete data.ownerId // impossible de toucher à cette clé - // } - // } - // if (perms.projectLocked) { - // if (!isAdmin) return new Forbidden403('Le projet est verrouillé') - // if (data.locked !== false) return new Forbidden403('Veuillez déverrouiler le projet pour le mettre à jour') - // } - - // if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() - - // const body = await updateProject(data, projectId, perms.user, req.id) - - // if (body instanceof ErrorResType) return body - // return { - // status: 200, - // body, - // } - // }, - - // // Reprovisionner un projet - // replayHooksForProject: async ({ request: req, params }) => { - // const projectId = params.projectId - // const perms = await authUser(req, { id: projectId }) - // const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - // if (!perms.projectPermissions && !isAdmin) return new NotFound404() - // if (!ProjectAuthorized.ReplayHooks(perms)) return new Forbidden403() - - // const body = await replayHooks({ - // projectId, - // userId: perms.user?.id, - // requestId: req.id, - // }) - - // if (body instanceof ErrorResType) return body - - // return { - // status: 204, - // body, - // } - // }, - - // // Archiver un projet - // archiveProject: async ({ request: req, params }) => { - // const projectId = params.projectId - // const perms = await authUser(req, { id: projectId }) - // const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions) - - // if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - // if (!perms.projectPermissions && !isAdmin) return new NotFound404() - // if (!ProjectAuthorized.Manage(perms)) return new Forbidden403() - - // const body = await archiveProject(projectId, perms.user, req.id) - // if (body instanceof ErrorResType) return body - - // return { - // status: 204, - // body, - // } - // }, - // // Récupérer les données de tous les projets pour export - // getProjectsData: async ({ request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - // const body = await generateProjectsData() - - // return { - // status: 200, - // body, - // } - // }, - - // bulkActionProject: async ({ request: req, body }) => { - // const perms = await authUser(req) - - // if (!perms.user) return new Unauthorized401('Cannot find requestor in database') - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // await bulkActionProject(body, perms.user, req.id) - - // return { - // status: 202, - // body: null, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts deleted file mode 100644 index 7923b2216..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/queries-index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from '@old-server/resources/admin-role/queries' -export * from '@old-server/resources/cluster/queries' -export * from '@old-server/resources/service-chain/queries' -export * from '@old-server/resources/environment/queries' -export * from '@old-server/resources/log/queries' -export * from '@old-server/resources/project/queries' -export * from '@old-server/resources/project-member/queries' -export * from '@old-server/resources/project-role/queries' -export * from '@old-server/resources/project-service/queries' -export * from '@old-server/resources/repository/queries' -export * from '@old-server/resources/user/queries' -export * from '@old-server/resources/stage/queries' -export * from '@old-server/resources/zone/queries' -export * from '@old-server/resources/system/settings/queries' diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts deleted file mode 100644 index 207548951..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/business.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Project, Repository, User } from '@prisma/client' -import type { CreateRepositoryBody, UpdateRepositoryBody } from '@cpn-console/shared' -import { addLogs, deleteRepository as deleteRepositoryQuery, getProjectInfosAndRepos, getProjectRepositories as getProjectRepositoriesQuery, initializeRepository, updateRepository as updateRepositoryQuery } from '@old-server/resources/queries-index' -import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors' -import { hook } from '@old-server/utils/hook-wrapper' - -export async function getProjectRepositories(projectId: Project['id']) { - return getProjectRepositoriesQuery(projectId) -} - -export async function syncRepository({ - repositoryId, - userId, - syncAllBranches, - branchName, - requestId, -}: { - repositoryId: Repository['id'] - userId: User['id'] - syncAllBranches: boolean - branchName?: string - requestId: string -}) { - const hookReply = await hook.misc.syncRepository(repositoryId, { syncAllBranches, branchName }) - await addLogs({ action: 'Sync Repository', data: hookReply, userId, requestId, projectId: hookReply.args.id }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services à la synchronisation du dépôt') - } - return null -} - -export async function createRepository({ - data, - userId, - requestId, -}: { - data: CreateRepositoryBody - userId: User['id'] - requestId: string -}) { - const project = await getProjectInfosAndRepos(data.projectId) - - if (project.repositories?.find(repo => repo.internalRepoName === data.internalRepoName)) return new BadRequest400(`Le nom du dépôt interne ${data.internalRepoName} existe déjà en base pour ce projet`) - const dbData = { ...data, isInfra: !!data.isInfra, isPrivate: !!data.isPrivate } - delete dbData.externalToken - - const repo = await initializeRepository(dbData) - const { results } = await hook.project.upsert(project.id, data.isPrivate - ? { - [repo.internalRepoName]: { - token: data.externalToken ?? '', - username: data.externalUserName ?? '', - }, - } - : undefined) - await addLogs({ action: 'Create Repository', data: results, userId, requestId, projectId: repo.projectId }) - if (results.failed) { - return new Unprocessable422('Echec des services lors de la création du dépôt') - } - - if (data.externalRepoUrl) { - await syncRepository({ repositoryId: repo.id, requestId, syncAllBranches: true, userId }) - } - return repo -} - -export async function updateRepository({ - repositoryId, - data, - userId, - requestId, -}: { - repositoryId: Repository['id'] - data: Partial - userId: User['id'] - requestId: string -}) { - const dbData = { ...data } - delete dbData.externalToken - const repo = await updateRepositoryQuery(repositoryId, dbData) - - const { results } = await hook.project.upsert(repo.projectId, { - [repo.internalRepoName]: { - username: repo.externalUserName ?? '', - token: data.externalToken ?? '', - }, - }) - await addLogs({ action: 'Update Repository', data: results, userId, requestId, projectId: repo.projectId }) - if (results.failed) { - return new Unprocessable422('Echec des services à la mise à jour du dépôt') - } - - return repo -} - -export async function deleteRepository({ - repositoryId, - userId, - requestId, - projectId, -}: { - repositoryId: Repository['id'] - userId: User['id'] - requestId: string - projectId: Project['id'] -}) { - await deleteRepositoryQuery(repositoryId) - - const { results } = await hook.project.upsert(projectId) - await addLogs({ action: 'Delete Repository', data: results, userId, requestId, projectId }) - if (results.failed) { - return new Unprocessable422('Echec des services à la suppression du dépôt') - } - return null -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts deleted file mode 100644 index ec861cb69..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/queries.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { Project, Repository } from '@prisma/client' -import prisma from '@old-server/prisma' - -// SELECT -export function getRepositoryById(id: Repository['id']) { - return prisma.repository.findUniqueOrThrow({ where: { id } }) -} - -export function getProjectRepositories(projectId: Project['id']) { - return prisma.repository.findMany({ where: { projectId } }) -} - -// CREATE -type RepositoryCreate = Pick & - Partial> - -export function initializeRepository({ projectId, internalRepoName, externalRepoUrl, isInfra, isPrivate, externalUserName, deployRevision, deployPath, helmValuesFiles }: RepositoryCreate) { - return prisma.repository.create({ - data: { - projectId, - internalRepoName, - externalRepoUrl, - externalUserName, - isInfra, - isPrivate, - deployRevision, - deployPath, - helmValuesFiles, - }, - }) -} - -export function getHookRepository(id: Repository['id']) { - return prisma.repository.findUniqueOrThrow({ - where: { - id, - }, - include: { - project: true, - }, - }) -} - -// UPDATE -export function updateRepository(id: Repository['id'], infos: Partial) { - return prisma.repository.update({ where: { id }, data: { ...infos } }) -} - -// DELETE -export async function deleteRepository(id: Repository['id']) { - const doesRepoExist = await getRepositoryById(id) - if (!doesRepoExist) throw new Error('Le dépôt interne demandé n\'existe pas en base pour ce projet') - return prisma.repository.delete({ where: { id } }) -} - -export function deleteAllRepositoryForProject(id: Project['id']) { - return prisma.repository.deleteMany({ where: { projectId: id } }) -} - -export function _createRepository(data: Parameters[0]['create']) { - return prisma.repository.upsert({ create: data, update: data, where: { id: data.id } }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts deleted file mode 100644 index a85a139d4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.spec.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { PROJECT_PERMS, repositoryContract } from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { atDates, getProjectMockInfos, getUserMockInfos } from '../../utils/mocks' -import { BadRequest400 } from '../../utils/errors' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessCreateMock = vi.spyOn(business, 'createRepository') -const businessUpdateMock = vi.spyOn(business, 'updateRepository') -const businessDeleteMock = vi.spyOn(business, 'deleteRepository') -const businessSyncMock = vi.spyOn(business, 'syncRepository') -const businessGetProjectRepositoriesMock = vi.spyOn(business, 'getProjectRepositories') - -describe('repositoryRouter tests', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const projectId = faker.string.uuid() - const repositoryId = faker.string.uuid() - const repositoryData = { - projectId, - externalRepoUrl: `${faker.internet.url()}.git`, - isPrivate: true, - externalToken: faker.string.alpha(), - externalUserName: faker.internet.username(), - isInfra: false, - internalRepoName: faker.string.alpha({ length: 5, casing: 'lower' }), - } - - describe('listRepositories', () => { - it('should return repositories for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessGetProjectRepositoriesMock.mockResolvedValueOnce([]) - - const response = await app.inject() - .get(repositoryContract.listRepositories.path) - .query({ projectId }) - .end() - - expect(businessGetProjectRepositoriesMock).toHaveBeenCalledWith(projectId) - expect(response.json()).toEqual([]) - expect(response.statusCode).toEqual(200) - }) - - it('should return empty for unauthorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.REPLAY_HOOKS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(repositoryContract.listRepositories.path) - .query({ projectId }) - .end() - - expect(businessGetProjectRepositoriesMock).toHaveBeenCalledTimes(0) - expect(response.json()).toEqual([]) - }) - }) - - describe('syncRepository', () => { - it('should synchronize repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(null) - - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(204) - expect(businessSyncMock).toHaveBeenCalledWith({ repositoryId, userId: user.user.id, branchName: 'main', requestId: expect.any(String), syncAllBranches: false }) - }) - - it('should return 403 for forbidden sync attempt', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.SEE_SECRETS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 403 for archived project', async () => { - const projectPerms = getProjectMockInfos({ projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessSyncMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(repositoryContract.syncRepository.path.replace(':repositoryId', repositoryId)) - .body({ branchName: 'main', syncAllBranches: false }) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('createRepository', () => { - it('should create repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...atDates }) - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(201) - expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData }) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(404) - }) - it('should return 403 for insuficient permissions', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(403) - }) - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(repositoryContract.createRepository.path) - .body(repositoryData) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) - - describe('updateRepository', () => { - const repoUpdateData = { isInfra: true } - it('should update repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...repoUpdateData, ...atDates }) - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(200) - expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData, ...repoUpdateData }) - }) - - it('should update repository and drop creds if is not private', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const repoUpdateData = { isPrivate: false, externalUserName: 'test' } - businessUpdateMock.mockResolvedValueOnce({ id: repositoryId, ...repositoryData, ...repoUpdateData, ...atDates }) - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(businessUpdateMock).toHaveBeenCalledWith({ data: { isPrivate: false }, repositoryId, requestId: expect.any(String), userId: user.user.id }) - expect(response.json()).toMatchObject({ id: repositoryId, ...repositoryData, ...repoUpdateData }) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if not enough permissions', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.LIST_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 if non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(404) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - }) - - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(repositoryContract.updateRepository.path.replace(':repositoryId', repositoryId)) - .body(repoUpdateData) - .end() - - expect(response.statusCode).toEqual(400) - }) - // TODO add tests about filtering - }) - - describe('deleteRepository', () => { - it('should delete repository for authorized user', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 if project is locked', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectLocked: true }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(403) - expect(response.json()).toEqual({ message: 'Le projet est verrouillé' }) - }) - - it('should return 403 if project is archived', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES, projectStatus: 'archived' }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.json()).toEqual({ message: 'Le projet est archivé' }) - expect(response.statusCode).toEqual(403) - }) - - it('should return 404 for non-member', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: 0n }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(404) - }) - it('should return 403 if not enough privilege', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_MEMBERS }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(403) - }) - it('should pass business error', async () => { - const projectPerms = getProjectMockInfos({ projectPermissions: PROJECT_PERMS.MANAGE_REPOSITORIES }) - const user = getUserMockInfos(false, undefined, projectPerms) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(repositoryContract.deleteRepository.path.replace(':repositoryId', repositoryId)) - .end() - - expect(response.statusCode).toEqual(400) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts deleted file mode 100644 index ddb740bd9..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/repository/router.ts +++ /dev/null @@ -1,138 +0,0 @@ -// import { AdminAuthorized, ProjectAuthorized, fakeToken, repositoryContract } from '@cpn-console/shared' -// import { - // createRepository, - // deleteRepository, - // getProjectRepositories, - // syncRepository, - // updateRepository, -// } from './business' -// import { serverInstance } from '@old-server/app' - -// import { filterObjectByKeys } from '@old-server/utils/queries-tools' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403, NotFound404, Unauthorized401 } from '@old-server/utils/errors' - -// export function repositoryRouter() { - // return serverInstance.router(repositoryContract, { - // // Récupérer tous les repositories d'un projet - // listRepositories: async ({ request: req, query }) => { - // const projectId = query.projectId - // const perms = await authUser(req, { id: projectId }) - - // const body = ProjectAuthorized.ListRepositories(perms) - // ? await getProjectRepositories(projectId) - // : [] - - // return { - // status: 200, - // body, - // } - // }, - - // // Synchroniser un repository - // syncRepository: async ({ request: req, params, body }) => { - // const { repositoryId } = params - // const perms = await authUser(req, { repositoryId }) - // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - // if (!perms.projectPermissions) return new NotFound404() - // if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const { syncAllBranches, branchName } = body - - // const resBody = await syncRepository({ repositoryId, userId: perms.user.id, branchName, requestId: req.id, syncAllBranches }) - // if (resBody instanceof ErrorResType) return resBody - - // return { - // status: 204, - // body: resBody, - // } - // }, - - // // Créer un repository - // createRepository: async ({ request: req, body: data }) => { - // const projectId = data.projectId - // const perms = await authUser(req, { id: projectId }) - - // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - // if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const body = await createRepository({ data, userId: perms.user.id, requestId: req.id }) - // if (body instanceof ErrorResType) return body - - // return { - // status: 201, - // body, - // } - // }, - - // // Mettre à jour un repository - // updateRepository: async ({ request: req, params, body }) => { - // const repositoryId = params.repositoryId - // const perms = await authUser(req, { repositoryId }) - - // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - // if (!perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions)) return new NotFound404() - // if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const keysAllowedForUpdate = [ - // 'externalRepoUrl', - // 'isPrivate', - // 'externalToken', - // 'externalUserName', - // 'isInfra', - // 'deployRevision', - // 'deployPath', - // 'helmValuesFiles', - // ] - // const data = filterObjectByKeys(body, keysAllowedForUpdate) - - // if (data.externalToken === fakeToken) { - // delete data.externalToken - // } - - // if (data.isPrivate === false) { - // delete data.externalToken - // delete data.externalUserName - // } - - // const resBody = await updateRepository({ repositoryId, data, userId: perms.user.id, requestId: req.id }) - // if (resBody instanceof ErrorResType) return resBody - - // return { - // status: 200, - // body: resBody, - // } - // }, - - // // Supprimer un repository - // deleteRepository: async ({ request: req, params }) => { - // const repositoryId = params.repositoryId - // const perms = await authUser(req, { repositoryId }) - - // if (!perms.user) return new Unauthorized401('Require to be requested from user not api key') - // if (!perms.projectPermissions) return new NotFound404() - // if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403() - // if (perms.projectLocked) return new Forbidden403('Le projet est verrouillé') - // if (perms.projectStatus === 'archived') return new Forbidden403('Le projet est archivé') - - // const body = await deleteRepository({ - // repositoryId, - // userId: perms.user.id, - // requestId: req.id, - // projectId: perms.projectId, - // }) - // if (body instanceof ErrorResType) return body - - // return { - // status: 204, - // body, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts deleted file mode 100644 index 1b08e2cc4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { - ServiceChain, - ServiceChainDetails, - ServiceChainFlows, -} from '@cpn-console/shared' -import { - serviceChainEnvironmentEnum, - serviceChainFlowStateEnum, - serviceChainLocationEnum, - serviceChainNetworkEnum, - serviceChainStateEnum, -} from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import axios from 'axios' -import type { Mock } from 'vitest' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -import { - getServiceChainDetails, - getServiceChainFlows, - listServiceChains, - retryServiceChain, - validateServiceChain, -} from './business' - -vi.mock('axios') - -let serviceChain: ServiceChain -let serviceChainDetails: ServiceChainDetails -let serviceChainFlows: ServiceChainFlows - -describe('test ServiceChain business logic', () => { - beforeEach(() => { - serviceChain = { - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement(serviceChainNetworkEnum), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - } - - serviceChainDetails = { - ...serviceChain, - validationId: faker.string.uuid(), - validatedBy: faker.helpers.maybe(() => faker.string.uuid()) || null, - ref: faker.string.uuid(), - location: faker.helpers.arrayElement(serviceChainLocationEnum), - targetAddress: faker.internet.ipv4(), - projectId: faker.string.uuid(), - env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), - subjectAlternativeName: faker.helpers.uniqueArray( - faker.internet.domainName, - 3, - ), - redirect: faker.datatype.boolean(), - antivirus: - faker.helpers.maybe(() => ({ - maxFileSize: faker.number.int(), - })) || null, // undefined is not wanted here - websocket: faker.datatype.boolean(), - ipWhiteList: faker.helpers - .uniqueArray(faker.internet.ipv4, 5) - .map(e => `${e}/32`), // We want a CIDR here - sslOutgoing: faker.datatype.boolean(), - } - - serviceChainFlows = { - reserve_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - create_cert: faker.helpers.maybe(() => ({ - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - })) || null, - call_exec: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - activate_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - dns_request: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - } - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('listServiceChains', () => { - it('should return a list of service chains', async () => { - const input = [serviceChain]; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }) - - const result = await listServiceChains() - - expect(result).toStrictEqual(input) - }) - }) - - describe('getServiceChainDetails', () => { - it('should return a service chain details', async () => { - const input = serviceChainDetails; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }) - - const result = await getServiceChainDetails(faker.string.uuid()) - - expect(result).toStrictEqual(input) - }) - }) - - describe('retryServiceChain', () => { - it('should trigger a service chain retry attempt', async () => { - const input = {}; - (axios.create as Mock).mockReturnValue({ - post: () => ({ data: input }), - }) - - const result = await retryServiceChain(faker.string.uuid()) - - expect(result.data).toStrictEqual(input) - }) - }) - - describe('validateServiceChain', () => { - it('should trigger a service chain validate attempt', async () => { - const input = {}; - (axios.create as Mock).mockReturnValue({ - post: () => ({ data: input }), - }) - - const result = await validateServiceChain(faker.string.uuid()) - - expect(result.data).toStrictEqual(input) - }) - }) - - describe('getServiceChainFlows', () => { - it('should return a service chain flows', async () => { - const input = serviceChainFlows; - (axios.create as Mock).mockReturnValue({ - get: () => ({ data: input }), - }) - - const result = await getServiceChainFlows(faker.string.uuid()) - - expect(result).toStrictEqual(input) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts deleted file mode 100644 index a63755604..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/business.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - getServiceChainDetails as getServiceChainDetailsQuery, - listServiceChains as listServiceChainsQuery, - retryServiceChain as retryServiceChainQuery, - validateServiceChain as validateServiceChainQuery, - getServiceChainFlows as getServiceChainFlowsQuery, -} from '@old-server/resources/queries-index' - -export async function listServiceChains() { - return listServiceChainsQuery() -} - -export async function getServiceChainDetails(serviceChainId: string) { - return getServiceChainDetailsQuery(serviceChainId) -} - -export async function retryServiceChain(serviceChainId: string) { - return retryServiceChainQuery(serviceChainId) -} - -export async function validateServiceChain(validationId: string) { - return validateServiceChainQuery(validationId) -} - -export async function getServiceChainFlows(serviceChainId: string) { - return getServiceChainFlowsQuery(serviceChainId) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts deleted file mode 100644 index 10713007c..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/queries.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - type ServiceChain, - ServiceChainDetailsSchema, - ServiceChainFlowsSchema, - ServiceChainListSchema, -} from '@cpn-console/shared' -import axios from 'axios' -import https from 'node:https' - -const openCDSEnvVar = 'OPENCDS_URL' -const openCDSTargetURL = process.env[openCDSEnvVar] -const openCDSDisabledErrorMessage = `OpenCDS is disabled, please set ${openCDSEnvVar} in your relevant .env file. See .env-example` - -function getClient() { - if (!openCDSTargetURL) { - throw new Error(openCDSDisabledErrorMessage) - } - return axios.create({ - baseURL: openCDSTargetURL, - httpsAgent: new https.Agent({ - rejectUnauthorized: - // We want it to be `false` only if it has explicitly - // been stated as "false" in the env vars - process.env.OPENCDS_API_TLS_REJECT_UNAUTHORIZED !== 'false', - }), - headers: { - 'X-API-Key': process.env.OPENCDS_API_TOKEN, - }, - }) -} - -export async function listServiceChains() { - return ServiceChainListSchema.parse( - (await getClient().get(`/requests`)).data, - ) -} - -export async function getServiceChainDetails( - serviceChainId: ServiceChain['id'], -) { - return ServiceChainDetailsSchema.parse( - (await getClient().get(`/requests/${serviceChainId}`)).data, - ) -} - -export async function retryServiceChain(serviceChainId: ServiceChain['id']) { - return await getClient().post(`/requests/${serviceChainId}/retry`) -} - -export async function validateServiceChain(validationId: string) { - return await getClient().post(`/validate/${validationId}`) -} - -export async function getServiceChainFlows(serviceChainId: ServiceChain['id']) { - return ServiceChainFlowsSchema.parse( - (await getClient().get(`/requests/${serviceChainId}/flows`)).data, - ) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts deleted file mode 100644 index c6ee037f7..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.spec.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ServiceChain, ServiceChainDetails, ServiceChainFlows } from '@cpn-console/shared' -import { - ServiceChainDetailsSchema, - ServiceChainFlowsSchema, - ServiceChainListSchema, - serviceChainContract, - serviceChainEnvironmentEnum, - serviceChainFlowStateEnum, - serviceChainLocationEnum, - serviceChainNetworkEnum, - serviceChainStateEnum, -} from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getUserMockInfos } from '../../utils/mocks' -import * as business from './business' - -vi.mock( - 'fastify-keycloak-adapter', - (await import('../../utils/mocks')).mockSessionPlugin, -) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListServiceChainsMock = vi.spyOn(business, 'listServiceChains') -const businessGetServiceChainDetailsMock = vi.spyOn(business, 'getServiceChainDetails') -const businessRetryServiceChainMock = vi.spyOn(business, 'retryServiceChain') -const businessValidateServiceChainMock = vi.spyOn(business, 'validateServiceChain') -const businessGetServiceChainsFlowsMock = vi.spyOn(business, 'getServiceChainFlows') - -describe('test ServiceChainContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - describe('listServiceChains', () => { - it('as non admin', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - businessListServiceChainsMock.mockResolvedValueOnce([]) - const response = await app - .inject() - .get(serviceChainContract.listServiceChains.path) - .end() - - expect(response.json()).toStrictEqual([]) - expect(response.statusCode).toEqual(200) - }) - it('as admin', async () => { - const user = getUserMockInfos(true) - const serviceChainList = faker.helpers.multiple(() => ({ - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement(serviceChainNetworkEnum), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - })) - - authUserMock.mockResolvedValueOnce(user) - - businessListServiceChainsMock.mockResolvedValueOnce(serviceChainList) - const response = await app - .inject() - .get(serviceChainContract.listServiceChains.path) - .end() - - expect(businessListServiceChainsMock).toHaveBeenCalledWith() - - expect(ServiceChainListSchema.parse(response.json())).toStrictEqual( - serviceChainList, - ) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('getServiceChainDetails', () => { - it('should return serviceChain details', async () => { - const serviceChainDetails: ServiceChainDetails = { - id: faker.string.uuid(), - state: faker.helpers.arrayElement(serviceChainStateEnum), - commonName: `${faker.string.alpha(3)}.${faker.string.alpha(3)}.minint.fr`, - pai: faker.string.alpha(3).toUpperCase(), - network: faker.helpers.arrayElement(serviceChainNetworkEnum), - createdAt: faker.date.recent(), - updatedAt: faker.date.recent(), - validationId: faker.string.uuid(), - validatedBy: faker.string.uuid(), - ref: faker.string.uuid(), - location: faker.helpers.arrayElement(serviceChainLocationEnum), - targetAddress: faker.internet.ipv4(), - projectId: faker.string.uuid(), - env: faker.helpers.arrayElement(serviceChainEnvironmentEnum), - subjectAlternativeName: faker.helpers.uniqueArray( - faker.internet.domainName, - 3, - ), - redirect: faker.datatype.boolean(), - antivirus: - faker.helpers.maybe(() => ({ - maxFileSize: faker.number.int(), - })) || null, // undefined is not wanted here - websocket: faker.datatype.boolean(), - ipWhiteList: faker.helpers - .uniqueArray(faker.internet.ipv4, 5) - .map(e => `${e}/32`), // We want a CIDR here - sslOutgoing: faker.datatype.boolean(), - } - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetServiceChainDetailsMock.mockResolvedValueOnce(serviceChainDetails) - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainDetails.path.replace( - ':serviceChainId', - serviceChainDetails.id, - ), - ) - .end() - - expect(ServiceChainDetailsSchema.parse(response.json())).toEqual( - serviceChainDetails, - ) - expect(response.statusCode).toEqual(200) - expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(1) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainDetails.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() - - expect(response.statusCode).toEqual(403) - expect(businessGetServiceChainDetailsMock).toHaveBeenCalledTimes(0) - }) - }) - - describe('retryServiceChain', () => { - it('should return 204', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessRetryServiceChainMock.mockResolvedValueOnce({ - status: 204, - body: undefined, - }) - const response = await app - .inject() - .post( - serviceChainContract.retryServiceChain.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() - - expect(response.body).toEqual('') - expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(1) - expect(response.statusCode).toEqual(204) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app - .inject() - .post( - serviceChainContract.retryServiceChain.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() - - expect(response.statusCode).toEqual(403) - expect(businessRetryServiceChainMock).toHaveBeenCalledTimes(0) - }) - }) - - describe('validateServiceChain', () => { - it('should return 204', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessValidateServiceChainMock.mockResolvedValueOnce({ - status: 204, - body: undefined, - }) - const response = await app - .inject() - .post( - serviceChainContract.validateServiceChain.path.replace( - ':validationId', - faker.string.uuid(), - ), - ) - .end() - - expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(1) - expect(response.body).toEqual('') - expect(response.statusCode).toEqual(204) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app - .inject() - .post( - serviceChainContract.validateServiceChain.path.replace( - ':validationId', - faker.string.uuid(), - ), - ) - .end() - - expect(businessValidateServiceChainMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('getServiceChainFlows', () => { - it('should return serviceChain flows', async () => { - const serviceChainFlows: ServiceChainFlows = { - reserve_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - create_cert: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - call_exec: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - activate_ip: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - dns_request: { - state: faker.helpers.arrayElement(serviceChainFlowStateEnum), - input: '{ "foo": 0, "bar": true, "qux": "test" }', - output: '{ "foo": 0, "bar": true, "qux": "test" }', - updatedAt: faker.date.recent(), - }, - } - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetServiceChainsFlowsMock.mockResolvedValueOnce(serviceChainFlows) - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainFlows.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() - - expect(ServiceChainFlowsSchema.parse(response.json())).toEqual( - serviceChainFlows, - ) - expect(response.statusCode).toEqual(200) - expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(1) - }) - it('should return 403 if not admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app - .inject() - .get( - serviceChainContract.getServiceChainFlows.path.replace( - ':serviceChainId', - faker.string.uuid(), - ), - ) - .end() - - expect(response.statusCode).toEqual(403) - expect(businessGetServiceChainsFlowsMock).toHaveBeenCalledTimes(0) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts deleted file mode 100644 index 61eff61fb..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-chain/router.ts +++ /dev/null @@ -1,90 +0,0 @@ -// import type { AsyncReturnType } from '@cpn-console/shared' -// import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared' -// import { - // listServiceChains as listServiceChainsBusiness, - // getServiceChainDetails as getServiceChainDetailsBusiness, - // retryServiceChain as retryServiceChainBusiness, - // validateServiceChain as validateServiceChainBusiness, - // getServiceChainFlows as getServiceChainFlowsBusiness, -// } from './business' -// import '@old-server/types/index' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { Forbidden403 } from '@old-server/utils/errors' - -// export function serviceChainRouter() { - // return serverInstance.router(serviceChainContract, { - // listServiceChains: async ({ request: req }) => { - // const { adminPermissions } = await authUser(req) - - // let body: AsyncReturnType = [] - // if (AdminAuthorized.isAdmin(adminPermissions)) { - // body = await listServiceChainsBusiness() - // } - - // return { - // status: 200, - // body, - // } - // }, - - // getServiceChainDetails: async ({ params, request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - // return new Forbidden403() - - // const serviceChainId = params.serviceChainId - // const serviceChainDetails - // = await getServiceChainDetailsBusiness(serviceChainId) - - // return { - // status: 200, - // body: serviceChainDetails, - // } - // }, - - // retryServiceChain: async ({ params, request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - // return new Forbidden403() - - // const serviceChainId = params.serviceChainId - // await retryServiceChainBusiness(serviceChainId) - - // return { - // status: 204, - // body: null, - // } - // }, - - // validateServiceChain: async ({ params, request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - // return new Forbidden403() - - // const serviceChainId = params.validationId - // await validateServiceChainBusiness(serviceChainId) - - // return { - // status: 204, - // body: null, - // } - // }, - - // getServiceChainFlows: async ({ params, request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - // return new Forbidden403() - - // const serviceChainId = params.serviceChainId - // const serviceChainFlows - // = await getServiceChainFlowsBusiness(serviceChainId) - - // return { - // status: 200, - // body: serviceChainFlows, - // } - // }, - - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts deleted file mode 100644 index fa61d5a6d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/business.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { services } from '@cpn-console/hooks' - -export function checkServicesHealth() { - return services.getStatus() -} - -export async function refreshServicesHealth() { - return Promise.all(services.refreshStatus()) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts deleted file mode 100644 index 1960f93bf..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { MonitorStatus, serviceContract } from '@cpn-console/shared' -import type { ServiceStatus } from '@cpn-console/hooks' -import app from '../../app' -import * as business from './business' -import { getUserMockInfos } from '../../utils/mocks' -import * as utilsController from '../../utils/controller' - -const authUserMock = vi.spyOn(utilsController, 'authUser') - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const businessCheckMock = vi.spyOn(business, 'checkServicesHealth') -const businessRefreshMock = vi.spyOn(business, 'refreshServicesHealth') - -describe('test serviceContract', () => { - const services: ServiceStatus[] = [{ interval: 1, lastUpdateTimestamp: 1, message: 'OK', name: 'A service', status: MonitorStatus.OK }] - const servicesComplete: ServiceStatus[] = [{ cause: 'error', interval: 1, lastUpdateTimestamp: 1, message: 'OK', name: 'A service', status: MonitorStatus.OK }] - - it('should return complete services, with cause', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessCheckMock.mockReturnValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end() - - expect(response.json()).toStrictEqual(servicesComplete) - expect(response.statusCode).toEqual(200) - }) - - it('should not return complete services, forbidden', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - businessCheckMock.mockReturnValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end() - - expect(response.statusCode).toEqual(403) - }) - - it('should return services', async () => { - businessCheckMock.mockReturnValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getServiceHealth.path) - .end() - - expect(response.json()).toStrictEqual(services) - expect(response.statusCode).toEqual(200) - }) - - it('should refresh services', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessRefreshMock.mockResolvedValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end() - - expect(response.json()).toStrictEqual(servicesComplete) - expect(response.statusCode).toEqual(200) - }) - - it('should refresh services, cause forbidden', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - businessRefreshMock.mockResolvedValue(servicesComplete) - const response = await app.inject() - .get(serviceContract.getCompleteServiceHealth.path) - .end() - - expect(response.statusCode).toEqual(403) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts deleted file mode 100644 index be17ad864..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/service-monitor/router.ts +++ /dev/null @@ -1,43 +0,0 @@ -// import { AdminAuthorized, serviceContract } from '@cpn-console/shared' -// import { checkServicesHealth, refreshServicesHealth } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { Forbidden403 } from '@old-server/utils/errors' - -// export function serviceMonitorRouter() { - // return serverInstance.router(serviceContract, { - // getServiceHealth: async () => { - // const serviceData = checkServicesHealth() - - // return { - // status: 200, - // body: serviceData, - // } - // }, - - // getCompleteServiceHealth: async ({ request: req }) => { - // const { adminPermissions } = await authUser(req) - - // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - // const serviceData = checkServicesHealth() - - // return { - // status: 200, - // body: serviceData, - // } - // }, - - // refreshServiceHealth: async ({ request: req }) => { - // const { adminPermissions } = await authUser(req) - // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - - // await refreshServicesHealth() - // const serviceData = checkServicesHealth() - - // return { - // status: 200, - // body: serviceData, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts deleted file mode 100644 index 9c4f914f9..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Environment, Stage } from '@prisma/client' -import prisma from '../../__mocks__/prisma' -import { BadRequest400, NotFound404 } from '../../utils/errors' -import { createStage, deleteStage, getStageAssociatedEnvironments, listStages, updateStage } from './business' - -describe('test stage busines logic', () => { - let stage: Stage - beforeEach(() => { - vi.resetAllMocks() - stage = { - id: faker.string.uuid(), - name: faker.company.name(), - } - }) - describe('createStage', () => { - it('should create a stage', async () => { - prisma.stage.findUnique.mockResolvedValue(null) - prisma.stage.create.mockResolvedValue({ id: stage.id } as Stage) - await createStage({ name: stage.name, clusterIds: [faker.string.uuid()] }) - expect(prisma.stage.update).toHaveBeenCalledTimes(1) - }) - it('should not create a stage, name conflict', async () => { - prisma.stage.findUnique.mockResolvedValue({ id: stage.id } as Stage) - const response = await createStage({ name: stage.name, clusterIds: [faker.string.uuid()] }) - expect(prisma.stage.update).toHaveBeenCalledTimes(0) - expect(response).instanceOf(BadRequest400) - }) - }) - - describe('updateStage', () => { - it('should update a stage', async () => { - const dbClusters = [{ id: faker.string.uuid() }] - const newClusters = [faker.string.uuid()] - prisma.stage.findUnique.mockResolvedValue({ ...stage, clusters: dbClusters } as Stage) - prisma.stage.update.mockResolvedValue({ id: stage.id } as Stage) - const response = await updateStage(stage.id, { name: stage.name, clusterIds: newClusters }) - expect(prisma.cluster.update).toHaveBeenCalledTimes(1) - expect(prisma.cluster.update).toHaveBeenCalledWith({ where: { id: dbClusters[0].id }, data: { - stages: { - disconnect: { - id: stage.id, - }, - }, - } }) - expect(prisma.stage.update).toHaveBeenCalledTimes(1) - expect(prisma.stage.update).toHaveBeenCalledWith({ where: { id: stage.id }, data: { - clusters: { - connect: [{ - id: newClusters[0], - }], - }, - } }) - expect(response.clusterIds).toBe(newClusters) - }) - it('should do nothing', async () => { - prisma.stage.findUnique.mockResolvedValue({ ...stage, clusters: [] } as Stage) - await updateStage(stage.id, { clusterIds: [], name: stage.name }) - expect(prisma.stage.update).toHaveBeenCalledTimes(0) - }) - it('should return not found', async () => { - prisma.stage.findUnique.mockResolvedValue(null) - const response = await updateStage(stage.id, { name: stage.name, clusterIds: [faker.string.uuid()] }) - expect(prisma.stage.update).toHaveBeenCalledTimes(0) - expect(response).instanceOf(NotFound404) - }) - }) - - describe('deleteStage', () => { - it('should delete a stage', async () => { - prisma.environment.findFirst.mockResolvedValue(null) - prisma.stage.delete.mockResolvedValue({ id: stage.id } as Stage) - await deleteStage(stage.id) - expect(prisma.stage.delete).toHaveBeenCalledTimes(1) - }) - it('should not delete a stage, environment attached', async () => { - prisma.environment.findFirst.mockResolvedValue({ id: faker.string.uuid() } as Environment) - const response = await deleteStage(stage.id) - expect(prisma.stage.delete).toHaveBeenCalledTimes(0) - expect(response).instanceOf(BadRequest400) - }) - }) - - describe('listStages', () => { - const clusterAssociated = [{ id: faker.string.uuid() }] - it('should list all stages (admin, no userId provided)', async () => { - prisma.stage.findMany.mockResolvedValue([{ clusters: clusterAssociated }] as unknown as Stage[]) - const response = await listStages() - expect(response[0].clusterIds).toStrictEqual([clusterAssociated[0].id]) - expect(prisma.stage.findMany).toHaveBeenCalledTimes(1) - expect(prisma.stage.findMany).toHaveBeenCalledWith({ include: { clusters: true } }) - }) - }) - - describe('getStageAssociatedEnvironments', () => { - it('should list all environments attached to a stage stages', async () => { - const envName = faker.string.alpha(8) - const projectSlug = faker.string.alpha(8) - const clusterLabel = faker.string.alpha(8) - const ownerEmail = faker.internet.email() - const envs = [{ name: envName, project: { slug: projectSlug, owner: { email: ownerEmail } }, cluster: { label: clusterLabel } }] - prisma.environment.findMany.mockResolvedValue(envs as unknown as Environment[]) - const response = await getStageAssociatedEnvironments(stage.id) - expect(response).toStrictEqual([{ - name: envName, - project: projectSlug, - owner: ownerEmail, - cluster: clusterLabel, - }]) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts deleted file mode 100644 index 24c63298a..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/business.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { Cluster, Stage } from '@prisma/client' -import type { CreateStageBody, UpdateStageBody } from '@cpn-console/shared' -import { - createStage as createStageQuery, - deleteStage as deleteStageQuery, - getAllStageIds, - getStageAssociatedEnvironmentById, - getStageById, - getStageByName, - linkClusterToStages as linkClusterToStagesQuery, - linkStageToClusters, - listStages as listStagesQuery, - removeClusterFromStage, - updateStageName, -} from '@old-server/resources/queries-index' -import { BadRequest400, NotFound404 } from '@old-server/utils/errors' -import prisma from '@old-server/prisma' - -export async function getStageAssociatedEnvironments(stageId: Stage['id']) { - const environments = await getStageAssociatedEnvironmentById(stageId) - return environments.map(env => ({ - project: env.project.slug, - name: env.name, - cluster: env.cluster.label, - owner: env.project.owner.email, - })) -} - -export async function createStage({ clusterIds = [], name }: CreateStageBody) { - const isNameTaken = await getStageByName(name) - if (isNameTaken) return new BadRequest400('Un type d\'environnement portant ce nom existe déjà') - - const stage = await createStageQuery({ name }) - - if (clusterIds.length) { - await linkStageToClusters(stage.id, clusterIds) - } - - return { - id: stage.id, - name: stage.name, - clusterIds, - } -} - -export async function updateStage(stageId: Stage['id'], { clusterIds, name }: UpdateStageBody) { - const dbStage = await getStageById(stageId) - if (!dbStage) return new NotFound404() - if (name !== dbStage.name) { - await updateStageName(stageId, name) - } - // Remove clusters - const dbClusters = dbStage.clusters - if (dbClusters?.length) { - const clustersToRemove = dbClusters.filter(dbCluster => !clusterIds.includes(dbCluster.id)) - for (const clusterToRemove of clustersToRemove) { - await removeClusterFromStage(clusterToRemove.id, stageId) - } - } - // Add clusters - if (clusterIds.length) { - await linkStageToClusters(stageId, clusterIds) - } - - return { - id: stageId, - name: name ?? dbStage.name, - clusterIds: clusterIds ?? dbStage.clusters.map(({ id }) => id), - } -} - -export async function deleteStage(stageId: Stage['id']) { - const attachedEnvironment = await prisma.environment.findFirst({ where: { stageId }, select: { id: true } }) - if (attachedEnvironment) return new BadRequest400('Impossible de supprimer le stage, des environnements en activité y ont souscrit') - - await deleteStageQuery(stageId) - return null -} - -export async function listStages() { - const stages = await listStagesQuery() - - return stages.map((stage) => { - return { - id: stage.id, - name: stage.name, - clusterIds: stage.clusters.map(({ id }) => id), - } - }) -} - -export async function linkClusterToStages(clusterId: Cluster['id'], stageIds: Stage['id'][], linkToAll: boolean = false) { - if (linkToAll === true) { - stageIds = await getAllStageIds() - } - await linkClusterToStagesQuery(clusterId, stageIds) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts deleted file mode 100644 index 1015099a2..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/queries.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { Cluster, Stage } from '@prisma/client' -import prisma from '@old-server/prisma' - -export function listStages() { - return prisma.stage.findMany({ - include: { - clusters: true, - }, - }) -} - -export async function getAllStageIds() { - return (await prisma.stage.findMany({ - select: { - id: true, - }, - })).map(({ id }) => id) -} - -export function getStageById(id: Stage['id']) { - return prisma.stage.findUnique({ - where: { id }, - include: { - clusters: true, - }, - }) -} - -export function getStageByIdOrThrow(id: Stage['id']) { - return prisma.stage.findUniqueOrThrow({ - where: { id }, - include: { - clusters: true, - }, - }) -} - -export function getStageAssociatedEnvironmentById(id: Stage['id']) { - return prisma.environment.findMany({ - where: { - stageId: id, - }, - select: { - name: true, - cluster: { - select: { - label: true, - }, - }, - project: { - select: { - name: true, - owner: true, - slug: true, - }, - }, - }, - }) -} - -export function getStageAssociatedEnvironmentLengthById(id: Stage['id']) { - return prisma.environment.count({ - where: { - stageId: id, - }, - }) -} - -export function getStageByName(name: Stage['name']) { - return prisma.stage.findUnique({ - where: { name }, - }) -} - -export function linkStageToClusters(id: Stage['id'], clusterIds: Cluster['id'][]) { - return prisma.stage.update({ - where: { - id, - }, - data: { - clusters: { - connect: clusterIds.map(clusterId => ({ id: clusterId })), - }, - }, - }) -} - -export function createStage({ name }: { name: Stage['name'] }) { - return prisma.stage.create({ - data: { - name, - }, - }) -} - -export function updateStageName(id: Stage['id'], name: Stage['name']) { - return prisma.stage.update({ - where: { - id, - }, - data: { - name, - }, - }) -} - -export function deleteStage(id: Stage['id']) { - return prisma.stage.delete({ - where: { id }, - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts deleted file mode 100644 index 34fb0088e..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Stage } from '@cpn-console/shared' -import { stageContract } from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getUserMockInfos } from '../../utils/mocks' -import { BadRequest400 } from '../../utils/errors' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMock = vi.spyOn(business, 'listStages') -const businessGetEnvironmentsMock = vi.spyOn(business, 'getStageAssociatedEnvironments') -const businessCreateMock = vi.spyOn(business, 'createStage') -const businessUpdateMock = vi.spyOn(business, 'updateStage') -const businessDeleteMock = vi.spyOn(business, 'deleteStage') - -describe('test stageContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listStages', () => { - it('should return list of stages', async () => { - const stages = [] - businessListMock.mockResolvedValueOnce(stages) - - const response = await app.inject() - .get(stageContract.listStages.path) - .end() - - expect(businessListMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(stages) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('getStageEnvironments', () => { - it('should return stage environments for admin', async () => { - const environments = [] - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetEnvironmentsMock.mockResolvedValueOnce(environments) - const response = await app.inject() - .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(environments) - expect(response.statusCode).toEqual(200) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessGetEnvironmentsMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(stageContract.getStageEnvironments.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(businessGetEnvironmentsMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('createStage', () => { - const stage: Stage = { id: faker.string.uuid(), name: faker.string.alpha({ length: 5 }), clusterIds: [] } - - it('should create and return stage for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(stage) - const response = await app.inject() - .post(stageContract.createStage.path) - .body(stage) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(stage) - expect(response.statusCode).toEqual(201) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(stageContract.createStage.path) - .body(stage) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(stageContract.createStage.path) - .body(stage) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('updateStage', () => { - const stageId = faker.string.uuid() - const stage = { name: faker.string.alpha({ length: 5 }), clusterIds: [] } - - it('should update and return stage for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: stageId, ...stage }) - const response = await app.inject() - .put(stageContract.updateStage.path.replace(':stageId', stageId)) - .body(stage) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ id: stageId, ...stage }) - expect(response.statusCode).toEqual(200) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(stageContract.updateStage.path.replace(':stageId', stageId)) - .body(stage) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(stageContract.updateStage.path.replace(':stageId', stageId)) - .body(stage) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('deleteStage', () => { - it('should delete stage for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(1) - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(stageContract.deleteStage.path.replace(':stageId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts deleted file mode 100644 index 6606d8ef4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/stage/router.ts +++ /dev/null @@ -1,88 +0,0 @@ -// import { AdminAuthorized, stageContract } from '@cpn-console/shared' -// import { - // createStage, - // deleteStage, - // getStageAssociatedEnvironments, - // listStages, - // updateStage, -// } from './business' -// import { serverInstance } from '@old-server/app' - -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' - -// export function stageRouter() { - // return serverInstance.router(stageContract, { - - // // Récupérer les types d'environnement disponibles - // listStages: async () => { - // const body = await listStages() - - // return { - // status: 200, - // body, - // } - // }, - - // // Récupérer les environnements associés au stage - // getStageEnvironments: async ({ request: req, params }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const stageId = params.stageId - // const body = await getStageAssociatedEnvironments(stageId) - // if (body instanceof ErrorResType) return body - - // return { - // status: 200, - // body, - // } - // }, - - // // Créer un stage - // createStage: async ({ request: req, body: data }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const body = await createStage(data) - // if (body instanceof ErrorResType) return body - - // return { - // status: 201, - // body, - // } - // }, - - // // Modifier une association stage / clusters - // updateStage: async ({ request: req, params, body: data }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const stageId = params.stageId - - // const body = await updateStage(stageId, data) - // if (body instanceof ErrorResType) return body - - // return { - // status: 200, - // body, - // } - // }, - - // // Supprimer un stage - // deleteStage: async ({ request: req, params }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const stageId = params.stageId - - // const body = await deleteStage(stageId) - // if (body instanceof ErrorResType) return body - - // return { - // status: 204, - // body, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts deleted file mode 100644 index ef2b0ea71..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest' -import prisma from '../../../__mocks__/prisma' -import { objToDb, updatePluginConfig } from './business' - -describe('test system/config business', () => { - const config = { test: { key1: 'value1' } } - it('should transform object to db row', () => { - const response = objToDb({ test: { key1: 'value1' } }) - expect(response).toEqual([{ pluginName: 'test', key: 'key1', value: 'value1' }]) - }) - describe('updatePluginConfig', () => { - it('should update', async () => { - prisma.adminPlugin.upsert.mockResolvedValue(null) - await updatePluginConfig(config) - }) - it('should update 0 items cause missing manifest', async () => { - // @ts-ignore - await updatePluginConfig({ test: { key: 1 } }) - expect(prisma.adminPlugin.upsert).toHaveBeenCalledTimes(0) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts deleted file mode 100644 index a891203a7..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/business.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { - PluginsUpdateBody, -} from '@cpn-console/shared' -import { editStrippers, populatePluginManifests, servicesInfos } from '@cpn-console/hooks' -import { - getAdminPlugin, - savePluginsConfig, -} from './queries' -import { BadRequest400 } from '@old-server/utils/errors' - -export type ConfigRecords = { - key: string - pluginName: string - value: string -}[] - -export function objToDb(obj: PluginsUpdateBody): ConfigRecords { - return Object.entries(obj) - .map(([pluginName, values]) => Object.entries(values) - .map(([key, value]) => ({ pluginName, key, value }))) - .flat() -} - -export async function getPluginsConfig() { - const globalConfig = await getAdminPlugin() - - return Object.values(servicesInfos).map(({ name, title, imgSrc, description }) => { - const manifest = populatePluginManifests({ - data: { - global: globalConfig, - }, - permissionTarget: 'admin', - pluginName: name, - select: { - global: true, - project: false, - }, - }) - return { imgSrc, title, name, manifest: manifest.global ?? [], description } - }).filter(plugin => plugin.manifest.length > 0) -} - -export async function updatePluginConfig(data: PluginsUpdateBody) { - const parsedData = editStrippers.global.safeParse(data) - if (!parsedData.success) return new BadRequest400(parsedData.error.message) - const records = objToDb(parsedData.data) - - await savePluginsConfig(records) - return null -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts deleted file mode 100644 index c038c0d80..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/queries.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { ConfigRecords } from './business' -import prisma from '@old-server/prisma' - -// CONFIG -export const getAdminPlugin = prisma.adminPlugin.findMany - -export async function savePluginsConfig(records: ConfigRecords) { - for (const { pluginName, key, value } of records) { - await prisma.adminPlugin.upsert({ - create: { - pluginName, - key, - value: String(value), - }, - update: { - key, - value: String(value), - pluginName, - }, - where: { - pluginName_key: { - pluginName, - key, - }, - }, - }) - } -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts deleted file mode 100644 index 472a102a6..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { systemPluginContract } from '@cpn-console/shared' -import app from '../../../app' -import * as utilsController from '../../../utils/controller' -import { getUserMockInfos } from '../../../utils/mocks' -import { BadRequest400 } from '../../../utils/errors' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetPluginsConfigMock = vi.spyOn(business, 'getPluginsConfig') -const businessUpdatePluginConfigMock = vi.spyOn(business, 'updatePluginConfig') - -describe('test systemPluginContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('getPluginsConfig', () => { - it('should return plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true) - const pluginsConfig = [] - - authUserMock.mockResolvedValueOnce(user) - businessGetPluginsConfigMock.mockResolvedValueOnce(pluginsConfig) - - const response = await app.inject() - .get(systemPluginContract.getPluginsConfig.path) - .end() - - expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(pluginsConfig) - expect(response.statusCode).toEqual(200) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(systemPluginContract.getPluginsConfig.path) - .end() - - expect(businessGetPluginsConfigMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('updatePluginsConfig', () => { - const newConfig = { plugin1: { keyId: 'value' } } - it('should update plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessUpdatePluginConfigMock.mockResolvedValueOnce(newConfig) - - const response = await app.inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end() - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith(newConfig) - expect(response.statusCode).toEqual(204) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end() - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - - it('should return error if business logic fails', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessUpdatePluginConfigMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - - const response = await app.inject() - .post(systemPluginContract.updatePluginsConfig.path) - .body(newConfig) - .end() - - expect(businessUpdatePluginConfigMock).toHaveBeenCalledWith(newConfig) - expect(response.statusCode).toEqual(400) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts deleted file mode 100644 index b08186cdb..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/config/router.ts +++ /dev/null @@ -1,36 +0,0 @@ -// import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared' -// import { getPluginsConfig, updatePluginConfig } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' - -// export function pluginConfigRouter() { - // return serverInstance.router(systemPluginContract, { - // // Récupérer les configurations plugins - // getPluginsConfig: async ({ request: req }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const services = await getPluginsConfig() - - // return { - // status: 200, - // body: services, - - // } - // }, - // // Mettre à jour les configurations plugins - // updatePluginsConfig: async ({ request: req, body }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const resBody = await updatePluginConfig(body) - // if (resBody instanceof ErrorResType) return resBody - - // return { - // status: 204, - // body: resBody, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts deleted file mode 100644 index 398cae734..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './router' diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts deleted file mode 100644 index 9ff040c6c..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { systemContract } from '@cpn-console/shared' -import app from '../../app' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) - -describe('system - router', () => { - it('should send application version', async () => { - const response = await app.inject() - .get(systemContract.getVersion.path) - .end() - - expect(response.statusCode).toBe(200) - expect(response.json()).toStrictEqual({ version: process.env.APP_VERSION || 'dev' }) - }) - - it('should send application health with status OK', async () => { - const response = await app.inject() - .get(systemContract.getHealth.path) - .end() - - expect(response.statusCode).toBe(200) - expect(response.json()).toStrictEqual({ status: 'OK' }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts deleted file mode 100644 index 79c53c7a2..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/router.ts +++ /dev/null @@ -1,21 +0,0 @@ -// import { systemContract } from '@cpn-console/shared' -// import { serverInstance } from '@old-server/app' -// import { appVersion } from '@old-server/utils/env' - -// export function systemRouter() { - // return serverInstance.router(systemContract, { - // getVersion: async () => ({ - // status: 200, - // body: { - // version: appVersion, - // }, - // }), - - // getHealth: async () => ({ - // status: 200, - // body: { - // status: 'OK', - // }, - // }), - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts deleted file mode 100644 index da540b473..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/business.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { UpsertSystemSettingBody } from '@cpn-console/shared' -import { - getSystemSettings as getSystemSettingsQuery, - upsertSystemSetting as upsertSystemSettingQuery, -} from './queries' - -export const getSystemSettings = (key?: string) => getSystemSettingsQuery({ key }) - -export const upsertSystemSetting = (newSystemSetting: UpsertSystemSettingBody) => upsertSystemSettingQuery(newSystemSetting) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts deleted file mode 100644 index 93a8ba02f..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/queries.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Prisma, SystemSetting } from '@prisma/client' -import prisma from '@old-server/prisma' - -export function upsertSystemSetting(newSystemSetting: SystemSetting) { - return prisma.systemSetting.upsert({ - create: { - ...newSystemSetting, - }, - update: { - value: newSystemSetting.value, - }, - where: { - key: newSystemSetting.key, - }, - }) -} - -export const getSystemSettings = (where?: Prisma.SystemSettingWhereInput) => prisma.systemSetting.findMany({ where }) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts deleted file mode 100644 index b48c3d6ea..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { systemSettingsContract } from '@cpn-console/shared' -import app from '../../../app' -import * as utilsController from '../../../utils/controller' -import { getUserMockInfos } from '../../../utils/mocks' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetSystemSettingsMock = vi.spyOn(business, 'getSystemSettings') -const businessUpsertSystemSettingMock = vi.spyOn(business, 'upsertSystemSetting') - -describe('test systemSettingsContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listSystemSettings', () => { - it('should return plugin configurations for authorized users', async () => { - const user = getUserMockInfos(true) - const systemSettings = [] - - authUserMock.mockResolvedValueOnce(user) - businessGetSystemSettingsMock.mockResolvedValueOnce(systemSettings) - - const response = await app.inject() - .get(systemSettingsContract.listSystemSettings.path) - .end() - - expect(businessGetSystemSettingsMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(systemSettings) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('upsertSystemSetting', () => { - const newConfig = { key: 'key1', value: 'value1' } - it('should update system setting, authorized users', async () => { - const user = getUserMockInfos(true) - - authUserMock.mockResolvedValueOnce(user) - businessUpsertSystemSettingMock.mockResolvedValueOnce(newConfig) - - const response = await app.inject() - .post(systemSettingsContract.upsertSystemSetting.path) - .body(newConfig) - .end() - - expect(businessUpsertSystemSettingMock).toHaveBeenCalledWith(newConfig) - expect(response.statusCode).toEqual(201) - }) - - it('should return 403 for unauthorized users', async () => { - const user = getUserMockInfos(false) - - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(systemSettingsContract.upsertSystemSetting.path) - .body(newConfig) - .end() - - expect(businessUpsertSystemSettingMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts deleted file mode 100644 index 977248157..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/system/settings/router.ts +++ /dev/null @@ -1,30 +0,0 @@ -// import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared' -// import { getSystemSettings, upsertSystemSetting } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { Forbidden403 } from '@old-server/utils/errors' - -// export function systemSettingsRouter() { - // return serverInstance.router(systemSettingsContract, { - // listSystemSettings: async ({ query }) => { - // const systemSettings = await getSystemSettings(query.key) - - // return { - // status: 200, - // body: systemSettings, - // } - // }, - - // upsertSystemSetting: async ({ request: req, body: data }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const systemSetting = await upsertSystemSetting(data) - - // return { - // status: 201, - // body: systemSetting, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts deleted file mode 100644 index 2b244fd0f..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import prisma from '../../__mocks__/prisma' -import type { UserDetails } from '../../types/index' -import { TokenInvalidReason, getMatchingUsers, getUsers, logViaSession, logViaToken, patchUsers } from './business' -import * as queries from './queries' - -const getUsersQueryMock = vi.spyOn(queries, 'getUsers') -const getMatchingUsersQueryMock = vi.spyOn(queries, 'getMatchingUsers') - -describe('test users business', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - const user = { - adminRoleIds: [], - createdAt: new Date(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - id: faker.string.uuid(), - lastName: faker.person.lastName(), - updatedAt: new Date(), - } - const projectId = faker.string.uuid() - const adminRoleId = faker.string.uuid() - describe('patchUsers', () => { - it('should do nothing', async () => { - prisma.user.update.mockResolvedValue(null) - - await patchUsers([]) - - expect(prisma.user.update).toHaveBeenCalledTimes(0) - }) - - it('should update a user adminRoleIds', async () => { - const userUpdated = { id: user.id, adminRoleIds: user.adminRoleIds } - - prisma.user.update.mockResolvedValue(user) - - prisma.user.findMany.mockResolvedValue([]) - - await patchUsers([userUpdated]) - expect(prisma.user.update).toHaveBeenCalledTimes(1) - expect(prisma.user.findMany).toHaveBeenCalledTimes(1) - - await patchUsers([userUpdated, userUpdated]) - expect(prisma.user.update).toHaveBeenCalledTimes(3) - }) - }) - describe('getUsers', () => { - it('should query without where', async () => { - prisma.user.update.mockResolvedValue(null) - - await getUsers({}) - - expect(getUsersQueryMock).toHaveBeenCalledTimes(1) - expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [] }) - }) - it('should query with filter adminRoleIds', async () => { - prisma.user.update.mockResolvedValue(null) - - await getUsers({ adminRoleIds: [adminRoleId] }) - - expect(getUsersQueryMock).toHaveBeenCalledTimes(1) - expect(getUsersQueryMock).toHaveBeenCalledWith({ AND: [{ adminRoleIds: { hasEvery: [adminRoleId] } }] }) - }) - }) - - describe('getMatchingUsers', () => { - const AND = [ - { - OR: [ - { - email: { - contains: 'abc', - mode: 'insensitive', - }, - }, - { - firstName: { - contains: 'abc', - mode: 'insensitive', - }, - }, - { - lastName: { - contains: 'abc', - mode: 'insensitive', - }, - }, - ], - }, - { - type: 'human', - }, - ] - it('should query only with letters ', async () => { - prisma.user.update.mockResolvedValue(null) - - await getMatchingUsers({ letters: 'abc' }) - - expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1) - expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND }) - }) - it('should query with letters and projectId', async () => { - prisma.user.update.mockResolvedValue(null) - - await getMatchingUsers({ letters: 'abc', notInProjectId: projectId }) - - expect(getMatchingUsersQueryMock).toHaveBeenCalledTimes(1) - expect(getMatchingUsersQueryMock).toHaveBeenCalledWith({ AND: [{ - projectMembers: { - none: { - projectId, - }, - }, - }, { - projectsOwned: { - none: { - id: projectId, - }, - }, - }].concat(AND) }) - }) - }) - describe('logViaSession', () => { - // ça ne teste pas tout mais c'est déjà bien hein - const adminRoles = [{ - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '', - permissions: 0n, - position: 0, - }, { - id: faker.string.uuid(), - name: faker.company.name(), - oidcGroup: '/admin', - permissions: 0n, - position: 0, - }] - const userToLog: UserDetails = { - id: faker.string.uuid(), - email: user.email, - firstName: user.firstName, - groups: [], - lastName: user.lastName, - } - it('should create user and return adminPerms', async () => { - prisma.adminRole.findMany.mockResolvedValue(adminRoles) - prisma.user.findUnique.mockResolvedValue(undefined) - prisma.user.create.mockResolvedValue(user) - prisma.user.update.mockResolvedValue(user) - const response = await logViaSession(userToLog) - expect(response.adminPerms).toBe(0n) - expect(prisma.user.create).toHaveBeenCalledTimes(1) - }) - it('should update user and return adminPerms', async () => { - prisma.adminRole.findMany.mockResolvedValue(adminRoles) - prisma.user.findUnique.mockResolvedValue(user) - prisma.user.update.mockResolvedValue(user) - const response = await logViaSession(userToLog) - expect(response.adminPerms).toEqual(0n) - expect(prisma.user.create).toHaveBeenCalledTimes(0) - }) - }) -}) - -describe('logViaToken', () => { - const nextYear = new Date() - const lastYear = new Date() - nextYear.setFullYear((new Date()).getFullYear() + 1) - lastYear.setFullYear((new Date()).getFullYear() - 1) - const baseToken = { - createdAt: new Date(), - hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', - id: faker.string.uuid(), - lastUse: null, - permissions: 2n, - userId: null, - status: 'active', - } as const - - it('should return identity', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken }) - const identity = await logViaToken('test') - expect(identity.adminPerms).toBe(2n) - }) - - it('should return identity based on pat', async () => { - const pat = structuredClone(baseToken) - delete pat.permissions - pat.owner = { adminRoleIds: null } - prisma.personalAccessToken.findFirst.mockResolvedValueOnce(pat) - const identity = await logViaToken('test') - expect(identity.adminPerms).toBe(0n) - }) - - it('should return identity, with expirationDate', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, expirationDate: nextYear }) - const identity = await logViaToken('test') - expect(identity.adminPerms).toBe(2n) - }) - - it('should return cause revoked', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, status: 'revoked' }) - const identity = await logViaToken('test') - expect(identity).toBe(TokenInvalidReason.INACTIVE) - }) - - it('should return cause expired', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce({ ...baseToken, expirationDate: lastYear }) - const identity = await logViaToken('test') - expect(identity).toBe(TokenInvalidReason.EXPIRED) - }) - - it('should return cause not found', async () => { - prisma.adminToken.findFirst.mockResolvedValueOnce(undefined) - const identity = await logViaToken('test') - expect(identity).toBe(TokenInvalidReason.NOT_FOUND) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts deleted file mode 100644 index f1135e2ef..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/business.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { createHash } from 'node:crypto' -import type { AdminRole, AdminToken, PersonalAccessToken, Prisma, User } from '@prisma/client' -import type { XOR, userContract } from '@cpn-console/shared' -import { getMatchingUsers as getMatchingUsersQuery, getUsers as getUsersQuery } from '@old-server/resources/queries-index' -import prisma from '@old-server/prisma' -import type { UserDetails } from '@old-server/types/index' -import { BadRequest400 } from '@old-server/utils/errors' - -export async function getUsers(query: typeof userContract.getAllUsers.query._type, relationType: 'OR' | 'AND' = 'AND') { - const whereInputs: Prisma.UserWhereInput[] = [] - if (query.adminRoleIds?.length) { - whereInputs.push({ adminRoleIds: { hasEvery: query.adminRoleIds } }) - } - if (query.adminRoles?.length) { - const roles = query.adminRoles - ? await prisma.adminRole.findMany({ where: { name: { in: query.adminRoles } } }) - : [] - - const adminRoleNameNotFound = query.adminRoles?.find(nameQueried => !roles.find(({ name }) => name === nameQueried)) - if (adminRoleNameNotFound) { - return new BadRequest400(`Unable to find adminRole ${adminRoleNameNotFound}`) - } - whereInputs.push({ adminRoleIds: { hasEvery: roles.map(({ id }) => id) } }) - } - if (query.memberOfIds) { - whereInputs.push({ - AND: query.memberOfIds.map(id => ({ - OR: [ - { projectsOwned: { some: { id } } }, - { ProjectMembers: { some: { project: { id } } } }, - ], - })), - }) - } - - return getUsersQuery({ [relationType]: whereInputs }) -} - -export async function getMatchingUsers(query: typeof userContract.getMatchingUsers.query._type) { - const AND: Prisma.UserWhereInput[] = [] - if (query.notInProjectId) { - AND.push({ projectMembers: { none: { projectId: query.notInProjectId } } }) - AND.push({ projectsOwned: { none: { id: query.notInProjectId } } }) - } - const filter = { contains: query.letters, mode: 'insensitive' } as const // Default value: default - if (query.letters) { - AND.push({ - OR: [{ - email: filter, - }, { - firstName: filter, - }, { - lastName: filter, - }], - }) - AND.push({ type: 'human' }) - } - - return getMatchingUsersQuery({ - AND, - }) -} - -export async function patchUsers(users: typeof userContract.patchUsers.body._type) { - for (const user of users) { - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - adminRoleIds: user.adminRoleIds, - }, - }) - } - - return prisma.user.findMany({ - where: { - id: { in: users.map(({ id }) => id) }, - }, - }) -} - -export enum TokenInvalidReason { - INACTIVE = 'Not active', - EXPIRED = 'Expired', - NOT_FOUND = 'Not authenticated', -} - -type UserTrial = Omit -export async function logViaSession({ id, email, groups, ...user }: UserTrial): Promise<{ user: User, adminPerms: bigint }> { - let userDb = await prisma.user.findUnique({ - where: { id }, - }) - - if (!userDb) { - userDb = await prisma.user.create({ data: { email, id, ...user, adminRoleIds: [], type: 'human' } }) - } - - const matchingAdminRoles = await prisma.adminRole.findMany({ - where: { OR: [{ oidcGroup: { in: groups } }, { id: { in: userDb.adminRoleIds } }] }, - }) - - const oidcRoleIds = matchingAdminRoles - .filter(({ oidcGroup }) => oidcGroup && groups.includes(oidcGroup)) - .map(({ id }) => id) - - const nonOidcRoleIds = matchingAdminRoles - .filter(({ oidcGroup, id }) => !oidcGroup && userDb.adminRoleIds.includes(id)) - .map(({ id }) => id) - - // On enregistre en bdd uniquement les roles de l'utilisateur - // qui ne viennent pas de keycloak - const updatedUser = await prisma.user.update({ where: { id }, data: { ...user, adminRoleIds: nonOidcRoleIds, lastLogin: (new Date()).toISOString() } }) - .then(user => ({ ...user, adminRoleIds: [...user.adminRoleIds, ...oidcRoleIds] })) - return { - user: updatedUser, - adminPerms: sumAdminPerms(matchingAdminRoles), - } -} - -type UserWithTokenId = Omit & { tokenId: string } -export async function logViaToken(pass: string): Promise<({ user: UserWithTokenId, adminPerms: bigint }) | TokenInvalidReason> { - const passHash = createHash('sha256').update(pass).digest('hex') - - let token: (XOR & { owner: User }) | TokenInvalidReason | undefined - const tokenLoginMethods = [findPersonalAccessToken, findAdminToken] - for (const tokenLoginMethod of tokenLoginMethods) { - token = await tokenLoginMethod(passHash) - if (token) { - break - } - } - - if (typeof token === 'string') { - return token - } - if (!token) { - return TokenInvalidReason.NOT_FOUND - } - - return { - user: { - ...token.owner, - tokenId: token.id, - }, - adminPerms: token?.permissions ?? await getAdminRolesAndSum(token.owner.adminRoleIds), - } -} - -function isTokenInvalid(token: AdminToken | PersonalAccessToken): TokenInvalidReason | undefined { - if (token.status !== 'active') { - return TokenInvalidReason.INACTIVE - } - const currentDate = new Date() - if (token.expirationDate && currentDate.getTime() > token.expirationDate?.getTime()) { - return TokenInvalidReason.EXPIRED - } -} - -function sumAdminPerms(roles: AdminRole[]): bigint { - if (!roles.length) { - return 0n - } - return roles.reduce((acc, curr) => acc | curr.permissions, 0n) -} - -async function getAdminRolesAndSum(roles: AdminRole['id'][] | null): Promise { - if (!roles?.length) { - return 0n - } - return sumAdminPerms(await prisma.adminRole.findMany({ - where: { id: { in: roles } }, - })) -} - -// List all token tpe authentication -async function findPersonalAccessToken(digest: string): Promise<(PersonalAccessToken & { owner: User }) | undefined | TokenInvalidReason> { - const token = await prisma.personalAccessToken.findFirst({ where: { hash: digest }, include: { owner: true } }) - if (!token) - return undefined - const invalidReason = isTokenInvalid(token) - if (invalidReason) { - return invalidReason - } - await prisma.personalAccessToken.update({ where: { id: token.id }, data: { lastUse: (new Date()).toISOString() } }) - await prisma.user.update({ where: { id: token.owner.id }, data: { lastLogin: (new Date()).toISOString() } }) - return token -} - -async function findAdminToken(digest: string): Promise<(AdminToken & { owner: User }) | undefined | TokenInvalidReason> { - const token = await prisma.adminToken.findFirst({ where: { hash: digest }, include: { owner: true } }) - if (!token) - return undefined - const invalidReason = isTokenInvalid(token) - if (invalidReason) { - return invalidReason - } - await prisma.adminToken.update({ where: { id: token.id }, data: { lastUse: (new Date()).toISOString() } }) - await prisma.user.update({ where: { id: token.userId }, data: { lastLogin: (new Date()).toISOString() } }) - return token -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts deleted file mode 100644 index 414b71abc..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/queries.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Prisma, User } from '@prisma/client' -import prisma from '@old-server/prisma' - -type UserCreate = Omit - -// SELECT -export const getUsers = (where?: Prisma.UserWhereInput) => prisma.user.findMany({ where }) - -export async function getUserInfos(id: User['id']) { - return prisma.user.findMany({ - where: { id }, - include: { - logs: true, - }, - }) -} - -export function getMatchingUsers(where: Prisma.UserWhereInput) { - return prisma.user.findMany({ - where, - take: 5, - }) -} - -export function getUserById(id: User['id']) { - return prisma.user.findUnique({ where: { id } }) -} - -export function getUserOrThrow(id: User['id']) { - return prisma.user.findUniqueOrThrow({ - where: { id }, - }) -} - -export function getUserByEmail(email: User['email']) { - return prisma.user.findUnique({ where: { email } }) -} - -// CREATE -export async function createUser({ id, email, firstName, lastName, type }: UserCreate) { - const user = await getUserByEmail(email) - if (user) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') - return prisma.user.create({ data: { id, email, firstName, lastName, type } }) -} - -// UPDATE -export async function updateUserById({ id, email, firstName, lastName }: UserCreate) { - const user = await getUserById(id) - const isEmailAlreadyTaken = await getUserByEmail(email) - if (!user) throw new Error('L\'utilisateur demandé n\'existe pas') - if (isEmailAlreadyTaken) throw new Error('Un utilisateur avec cette adresse e-mail existe déjà') - if (user && !isEmailAlreadyTaken) { - return prisma.user.update({ where: { id }, data: { email, firstName, lastName } }) - } -} - -// TECH -export function _createUser(data: Prisma.UserCreateInput) { - return prisma.user.upsert({ where: { id: data.id }, create: data, update: data }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts deleted file mode 100644 index 5768e869e..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.spec.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { userContract } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getUserMockInfos, setRequestor } from '../../utils/mocks' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessGetMatchingMock = vi.spyOn(business, 'getMatchingUsers') -const businessLogViaSessionMock = vi.spyOn(business, 'logViaSession') -const businessGetUsersMock = vi.spyOn(business, 'getUsers') -const businessPatchMock = vi.spyOn(business, 'patchUsers') - -describe('test userContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('getMatchingUsers', () => { - it('should return matching users', async () => { - const usersMatching = [] - businessGetMatchingMock.mockResolvedValueOnce(usersMatching) - - const response = await app.inject() - .get(userContract.getMatchingUsers.path) - .query({ letters: faker.person.fullName() }) - .end() - - expect(businessGetMatchingMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(usersMatching) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('auth', () => { - it('should return logged user', async () => { - const user = { - id: faker.string.uuid(), - adminRoleIds: [], - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - type: 'human', - lastName: faker.person.lastName(), - } - setRequestor(user) - businessLogViaSessionMock.mockResolvedValueOnce({ user, adminPerms: 0n }) - - const response = await app.inject() - .get(userContract.auth.path) - .end() - - expect(businessLogViaSessionMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(user) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('getAllUsers', () => { - it('should return all users for admin', async () => { - const user = getUserMockInfos(true) - const users = [] - authUserMock.mockResolvedValueOnce(user) - businessGetUsersMock.mockResolvedValueOnce(users) - - const response = await app.inject() - .get(userContract.getAllUsers.path) - .query({ role: 'admin' }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetUsersMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(users) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .get(userContract.getAllUsers.path) - .query({ role: 'admin' }) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessGetUsersMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('patchUsers', () => { - const usersPatchData = [{ - id: faker.string.uuid(), - adminRoleIds: [], - }] - const usersReturn = [{ - id: faker.string.uuid(), - adminRoleIds: [], - createdAt: (new Date()).toISOString(), - updatedAt: (new Date()).toISOString(), - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - type: 'human', - }] - - it('should patch and return users for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessPatchMock.mockResolvedValueOnce(usersReturn) - const response = await app.inject() - .patch(userContract.patchUsers.path) - .body(usersPatchData) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessPatchMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(usersReturn) - expect(response.statusCode).toEqual(200) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .patch(userContract.patchUsers.path) - .body(usersPatchData) - .end() - - expect(authUserMock).toHaveBeenCalledTimes(1) - expect(businessPatchMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts deleted file mode 100644 index d36a40a81..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/router.ts +++ /dev/null @@ -1,63 +0,0 @@ -// import { AdminAuthorized, userContract } from '@cpn-console/shared' -// import { - // getMatchingUsers, - // getUsers, - // logViaSession, - // patchUsers, -// } from './business' -// import '@old-server/types/index' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' - -// export function userRouter() { - // return serverInstance.router(userContract, { - // getMatchingUsers: async ({ query }) => { - // const usersMatching = await getMatchingUsers(query) - - // return { - // status: 200, - // body: usersMatching, - // } - // }, - - // auth: async ({ request: req }) => { - // const user = req.session.user - - // if (!user) return new Unauthorized401() - - // const { user: body } = await logViaSession(user) - - // return { - // status: 200, - // body, - // } - // }, - - // getAllUsers: async ({ request: req, query: { relationType, ...query } }) => { - // const perms = await authUser(req) - - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const body = await getUsers(query, relationType) - // if (body instanceof ErrorResType) return body - - // return { - // status: 200, - // body, - // } - // }, - - // patchUsers: async ({ request: req, body }) => { - // const perms = await authUser(req) - // if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403() - - // const users = await patchUsers(body) - - // return { - // status: 200, - // body: users, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts deleted file mode 100644 index 5d7277526..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/business.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createHash } from 'node:crypto' -import type { personalAccessTokenContract } from '@cpn-console/shared' -import { generateRandomPassword, isAtLeastTomorrow } from '@cpn-console/shared' -import type { AdminToken, User } from '@prisma/client' -import prisma from '../../../prisma' -import { BadRequest400 } from '@old-server/utils/errors' - -export async function listTokens(userId: User['id']) { - return prisma.personalAccessToken.findMany({ - omit: { hash: true }, - include: { owner: true }, - orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], - where: { userId }, - }) -} - -export async function createToken(data: typeof personalAccessTokenContract.createPersonalAccessToken.body._type, userId: User['id']) { - if (data.expirationDate && !isAtLeastTomorrow(new Date(data.expirationDate))) { - return new BadRequest400('Date d\'expiration trop courte') - } - const password = generateRandomPassword(48, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-') - const hash = createHash('sha256').update(password).digest('hex') - const token = await prisma.personalAccessToken.create({ - data: { - ...data, - hash, - expirationDate: new Date(data.expirationDate), - userId, - }, - omit: { hash: true }, - include: { owner: true }, - }) - return { - ...token, - password, - } -} - -export async function deleteToken(id: AdminToken['id'], userId: User['id']) { - const token = await prisma.personalAccessToken.findUnique({ - where: { - id, - userId, - }, - }) - if (token) { - return prisma.personalAccessToken.delete({ - where: { id }, - }) - } -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts deleted file mode 100644 index c24d8747b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/user/tokens/router.ts +++ /dev/null @@ -1,48 +0,0 @@ -// import { personalAccessTokenContract } from '@cpn-console/shared' - -// import '@old-server/types/index' -// import { createToken, deleteToken, listTokens } from './business' -// import { serverInstance } from '@old-server/app' -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403 } from '@old-server/utils/errors' - -// export function personalAccessTokenRouter() { - // return serverInstance.router(personalAccessTokenContract, { - // listPersonalAccessTokens: async ({ request: req }) => { - // const perms = await authUser(req) - - // if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - // const body = await listTokens(perms.user.id) - - // return { - // status: 200, - // body, - // } - // }, - - // createPersonalAccessToken: async ({ request: req, body: data }) => { - // const perms = await authUser(req) - - // if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - // const body = await createToken(data, perms.user.id) - // if (body instanceof ErrorResType) return body - - // return { - // status: 201, - // body, - // } - // }, - - // deletePersonalAccessToken: async ({ request: req, params }) => { - // const perms = await authUser(req) - - // if (!perms.user?.id || perms.user?.type !== 'human') return new Forbidden403() - // await deleteToken(params.tokenId, perms.user.id) - - // return { - // status: 204, - // body: null, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts deleted file mode 100644 index 938658e08..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Cluster, Zone } from '@prisma/client' -import prisma from '../../__mocks__/prisma' -import { BadRequest400 } from '../../utils/errors' -import { hook } from '../../__mocks__/utils/hook-wrapper' -import { createZone, deleteZone, listZones, updateZone } from './business' -import * as queries from './queries' - -const userId = faker.string.uuid() -const reqId = faker.string.uuid() -const linkZoneToClustersMock = vi.spyOn(queries, 'linkZoneToClusters') -vi.mock('../../utils/hook-wrapper', async () => ({ - hook, -})) - -describe('test zone business', () => { - const zones: Zone[] = [{ - id: faker.string.uuid(), - label: faker.company.name(), - argocdUrl: faker.internet.url(), - createdAt: new Date(), - updatedAt: new Date(), - description: faker.lorem.lines(1), - slug: faker.string.alphanumeric(5), - }, { - id: faker.string.uuid(), - label: faker.company.name(), - argocdUrl: faker.internet.url(), - createdAt: new Date(), - updatedAt: new Date(), - description: faker.lorem.lines(1), - slug: faker.string.alphanumeric(6), - }] - - const clusters: Pick[] = [ - { id: faker.string.uuid() }, - { id: faker.string.uuid() }, - ] - - beforeEach(() => { - vi.resetAllMocks() - }) - describe('listZones', () => { - it('should return zones', async () => { - prisma.zone.findMany.mockResolvedValueOnce(zones) - - const response = await listZones() - expect(response).toEqual(zones) - }) - }) - describe('createZone', () => { - it('should create zone without description and clusterIds', async () => { - const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl } - - hook.zone.upsert.mockResolvedValue({}) - prisma.zone.create.mockResolvedValueOnce(zones[0]) - const response = await createZone(newZone, userId, reqId) - - expect(response).toEqual(zones[0]) - expect(prisma.zone.create).toHaveBeenCalledWith({ - data: { - slug: newZone.slug, - label: newZone.label, - argocdUrl: newZone.argocdUrl, - description: undefined, - }, - }) - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0) - }) - it('should create zone with description and clusterIds', async () => { - const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl, clusterIds: clusters.map(({ id }) => id), description: faker.lorem.lines(2) } - - hook.zone.upsert.mockResolvedValue({}) - prisma.zone.create.mockResolvedValueOnce(zones[0]) - const response = await createZone(newZone, userId, reqId) - - expect(response).toEqual(zones[0]) - expect(prisma.zone.create).toHaveBeenCalledWith({ - data: { - description: newZone.description, - label: newZone.label, - argocdUrl: newZone.argocdUrl, - slug: newZone.slug, - }, - }) - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(1) - }) - it('should not create zone, conflict label', async () => { - const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl } - - prisma.zone.findUnique.mockResolvedValueOnce(zones[0]) - prisma.zone.create.mockResolvedValueOnce(zones[0]) - const response = await createZone(newZone, userId, reqId) - - expect(response).instanceOf(BadRequest400) - expect(prisma.zone.create).toHaveBeenCalledTimes(0) - expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0) - }) - }) - describe('updateZone', () => { - it('should filter keys and update zone', async () => { - prisma.zone.update.mockResolvedValueOnce(zones[0]) - hook.zone.upsert.mockResolvedValue({}) - await updateZone(zones[0].id, { - description: '', - label: zones[0].label, - argocdUrl: zones[0].argocdUrl, - extraKey: 1, - }, userId, reqId) - expect(prisma.zone.update).toHaveBeenCalledWith({ where: { id: zones[0].id }, data: { - description: '', - label: zones[0].label, - argocdUrl: zones[0].argocdUrl, - } }) - }) - }) - describe('deleteZone', () => { - it('should not delete zone, cluster attached', async () => { - prisma.cluster.findFirst.mockResolvedValueOnce(clusters[0]) - const response = await deleteZone(zones[0].id, userId, reqId) - expect(response).instanceOf(BadRequest400) - expect(prisma.cluster.delete).toHaveBeenCalledTimes(0) - }) - it('should delete zone', async () => { - prisma.cluster.findFirst.mockResolvedValueOnce(undefined) - hook.zone.delete.mockResolvedValue({}) - const response = await deleteZone(zones[0].id, userId, reqId) - expect(response).toEqual(null) - expect(prisma.zone.delete).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts deleted file mode 100644 index 293dc8675..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/business.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { User, Zone } from '@cpn-console/shared' -import { addLogs } from '../queries-index' -import { linkZoneToClusters } from './queries' -import { BadRequest400, Unprocessable422 } from '@old-server/utils/errors' -import prisma from '@old-server/prisma' -import { hook } from '@old-server/utils/hook-wrapper' - -export const listZones = prisma.zone.findMany - -export async function createZone( - data: { slug: string, label: string, argocdUrl: string, description?: string | null, clusterIds?: string[] }, - userId: User['id'], - requestId: string, -) { - const { slug, label, argocdUrl, description, clusterIds } = data - - const existingZone = await prisma.zone.findUnique({ - where: { slug }, - }) - - if (existingZone) return new BadRequest400(`Une zone portant le nom ${slug} existe déjà.`) - const zone = await prisma.zone.create({ - data: { - slug, - label, - argocdUrl, - description, - }, - }) - if (clusterIds) { - await linkZoneToClusters(zone.id, clusterIds) - } - const hookReply = await hook.zone.upsert(zone.id) - await addLogs({ action: 'Create zone', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services lors de la création de la zone') - } - return zone -} - -export async function updateZone( - zoneId: Zone['id'], - data: Pick, - userId: User['id'], - requestId: string, -) { - const { label, argocdUrl, description } = data - - const updatedZone = await prisma.zone.update({ - where: { - id: zoneId, - }, - data: { - label, - argocdUrl, - description, - }, - }) - const hookReply = await hook.zone.upsert(updatedZone.id) - await addLogs({ action: 'Update zone', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services lors de la mise à jour de la zone') - } - return updatedZone -} - -export async function deleteZone(zoneId: Zone['id'], userId: User['id'], requestId: string) { - const attachedCluster = await prisma.cluster.findFirst({ where: { zoneId }, select: { id: true } }) - if (attachedCluster) return new BadRequest400('Vous ne pouvez supprimer cette zone, car des clusters y sont associés.') - - const hookReply = await hook.zone.delete(zoneId) - await addLogs({ action: 'Delete zone', data: hookReply, userId, requestId }) - if (hookReply.failed) { - return new Unprocessable422('Echec des services lors de la suppression de la zone') - } - await prisma.zone.delete({ where: { id: zoneId } }) - return null -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts deleted file mode 100644 index 9f3f7b355..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/queries.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Cluster, Zone } from '@prisma/client' -import prisma from '@old-server/prisma' - -export function getZoneByIdOrThrow(id: Zone['id']) { - return prisma.zone.findUniqueOrThrow({ - where: { id }, - }) -} - -export function linkZoneToClusters(zoneId: Zone['id'], clusterIds: Cluster['id'][]) { - return prisma.zone.update({ - where: { - id: zoneId, - }, - data: { - clusters: { - connect: clusterIds.map(clusterId => ({ id: clusterId })), - }, - }, - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts deleted file mode 100644 index 0bf4df4d4..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { faker } from '@faker-js/faker' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Zone } from '@cpn-console/shared' -import { zoneContract } from '@cpn-console/shared' -import app from '../../app' -import * as utilsController from '../../utils/controller' -import { getUserMockInfos } from '../../utils/mocks' -import { BadRequest400 } from '../../utils/errors' -import * as business from './business' - -vi.mock('fastify-keycloak-adapter', (await import('../../utils/mocks')).mockSessionPlugin) -const authUserMock = vi.spyOn(utilsController, 'authUser') -const businessListMock = vi.spyOn(business, 'listZones') -const businessCreateMock = vi.spyOn(business, 'createZone') -const businessUpdateMock = vi.spyOn(business, 'updateZone') -const businessDeleteMock = vi.spyOn(business, 'deleteZone') - -describe('test zoneContract', () => { - beforeEach(() => { - vi.resetAllMocks() - }) - - describe('listZones', () => { - it('should return list of zones', async () => { - const zones = [] - businessListMock.mockResolvedValueOnce(zones) - - const response = await app.inject() - .get(zoneContract.listZones.path) - .end() - - expect(businessListMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(zones) - expect(response.statusCode).toEqual(200) - }) - }) - - describe('createZone', () => { - const zone = { id: faker.string.uuid(), label: faker.string.alpha({ length: 5 }), argocdUrl: faker.internet.url(), slug: faker.string.alpha({ length: 5, casing: 'lower' }), description: '' } - - it('should create and return zone for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(zone) - const response = await app.inject() - .post(zoneContract.createZone.path) - .body(zone) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual(zone) - expect(response.statusCode).toEqual(201) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessCreateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .post(zoneContract.createZone.path) - .body(zone) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .post(zoneContract.createZone.path) - .body(zone) - .end() - - expect(businessCreateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('updateZone', () => { - const zoneId = faker.string.uuid() - const zone: Omit = { label: faker.string.alpha({ length: 5 }), slug: faker.string.alpha({ length: 5, casing: 'lower' }), argocdUrl: faker.internet.url(), description: '' } - - it('should update and return zone for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce({ id: zoneId, ...zone }) - const response = await app.inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(1) - expect(response.json()).toEqual({ id: zoneId, ...zone }) - expect(response.statusCode).toEqual(200) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessUpdateMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .put(zoneContract.updateZone.path.replace(':zoneId', zoneId)) - .body(zone) - .end() - - expect(businessUpdateMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) - - describe('deleteZone', () => { - it('should delete zone for admin', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(null) - const response = await app.inject() - .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(1) - expect(response.body).toBeFalsy() - expect(response.statusCode).toEqual(204) - }) - it('should pass business error', async () => { - const user = getUserMockInfos(true) - authUserMock.mockResolvedValueOnce(user) - - businessDeleteMock.mockResolvedValueOnce(new BadRequest400('une erreur')) - const response = await app.inject() - .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) - .end() - - expect(response.statusCode).toEqual(400) - }) - it('should return 403 for non-admin', async () => { - const user = getUserMockInfos(false) - authUserMock.mockResolvedValueOnce(user) - - const response = await app.inject() - .delete(zoneContract.deleteZone.path.replace(':zoneId', faker.string.uuid())) - .end() - - expect(businessDeleteMock).toHaveBeenCalledTimes(0) - expect(response.statusCode).toEqual(403) - }) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts b/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts deleted file mode 100644 index ba1a45a3b..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/resources/zone/router.ts +++ /dev/null @@ -1,64 +0,0 @@ -// import { AdminAuthorized, zoneContract } from '@cpn-console/shared' -// import { createZone, deleteZone, listZones, updateZone } from './business' -// import { serverInstance } from '@old-server/app' - -// import { authUser } from '@old-server/utils/controller' -// import { ErrorResType, Forbidden403, Unauthorized401 } from '@old-server/utils/errors' - -// export function zoneRouter() { - // return serverInstance.router(zoneContract, { - // listZones: async () => { - // const zones = await listZones() - - // return { - // status: 200, - // body: zones, - // } - // }, - - // createZone: async ({ request: req, body: data }) => { - // const { user, adminPermissions } = await authUser(req) - // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - // if (!user) return new Unauthorized401('Require to be requested from user not api key') - - // const body = await createZone(data, user.id, req.id) - // if (body instanceof ErrorResType) return body - - // return { - // status: 201, - // body, - // } - // }, - - // updateZone: async ({ request: req, params, body: data }) => { - // const { user, adminPermissions } = await authUser(req) - // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - // if (!user) return new Unauthorized401('Require to be requested from user not api key') - - // const zoneId = params.zoneId - - // const body = await updateZone(zoneId, data, user.id, req.id) - // if (body instanceof ErrorResType) return body - - // return { - // status: 200, - // body, - // } - // }, - - // deleteZone: async ({ request: req, params }) => { - // const { user, adminPermissions } = await authUser(req) - // if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403() - // if (!user) return new Unauthorized401('Require to be requested from user not api key') - // const zoneId = params.zoneId - - // const body = await deleteZone(zoneId, user.id, req.id) - // if (body instanceof ErrorResType) return body - - // return { - // status: 204, - // body, - // } - // }, - // }) -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts deleted file mode 100644 index 1ec57ceda..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { exitGracefully, handleExit } from './server' -import { closeConnections } from './connect' -import { logger } from './app' - -vi.mock('fastify-keycloak-adapter', (await import('./utils/mocks')).mockSessionPlugin) -vi.mock('./init/db/index', () => ({ initDb: vi.fn() })) -vi.mock('./connect') - -process.exit = vi.fn() - -vi.mock('./prepare-app', () => { - const app = { - listen: vi.fn(), - close: vi.fn(async () => {}), - } - return { - getPreparedApp: () => Promise.resolve(app), - } -}) -vi.spyOn(logger, 'info') -vi.spyOn(logger, 'warn') -vi.spyOn(logger, 'error') -vi.spyOn(logger, 'fatal') -vi.spyOn(logger, 'debug') - -describe('server', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should call closeConnections without parameter', async () => { - await exitGracefully() - - expect(closeConnections).toHaveBeenCalledTimes(1) - expect(closeConnections.mock.calls[0]).toHaveLength(0) - expect(logger.error).toHaveBeenCalledTimes(0) - }) - - it('should log an error', async () => { - await exitGracefully(new Error('error')) - - expect(closeConnections).toHaveBeenCalledTimes(1) - expect(closeConnections.mock.calls[0]).toHaveLength(0) - expect(logger.fatal).toHaveBeenCalledTimes(1) - expect(logger.fatal.mock.calls[0][0]).toBeInstanceOf(Error) - expect(logger.info).toHaveBeenCalledTimes(2) - }) - - it('should call process.on 4 times', () => { - const processOn = vi.spyOn(process, 'on') - - handleExit() - - expect(processOn).toHaveBeenCalledTimes(5) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts b/apps/server-nestjs/src/cpin-module/old-server/src/server.ts deleted file mode 100644 index d42849fb5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/server.ts +++ /dev/null @@ -1,44 +0,0 @@ -// import { getPreparedApp } from './prepare-app' -// import { closeConnections } from './connect' -// import { isCI, isDev, isDevSetup, isProd, isTest, port } from './utils/env' -// import { logger } from './app' - -// const app = await getPreparedApp() - -// try { - // await app.listen({ host: '0.0.0.0', port: +(port ?? 8080) }) -// } catch (error) { - // logger.error(error) - // process.exit(1) -// } - -// logger.debug({ isDev, isTest, isCI, isDevSetup, isProd }) - -// export async function exitGracefully(error?: Error) { - // if (error instanceof Error) { - // logger.fatal(error) - // } - // await app.close() - // logger.info('Closing connections...') - // await closeConnections() - // logger.info('Exiting...') - // process.exit(error instanceof Error ? 1 : 0) -// } - -// function logExitCode(code: number) { - // logger.warn(`received signal: ${code}`) -// } - -// function logUnhandledRejection(reason: unknown, promise: Promise) { - // logger.error({ message: 'Unhandled Rejection', promise, reason }) -// } - -// export function handleExit() { - // process.on('exit', logExitCode) - // process.on('SIGINT', exitGracefully) - // process.on('SIGTERM', exitGracefully) - // process.on('uncaughtException', exitGracefully) - // process.on('unhandledRejection', logUnhandledRejection) -// } - -// handleExit() diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts deleted file mode 100644 index 1ffe757f3..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/business.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { type SharedSafeParseReturnType, parseZodError } from '@cpn-console/shared' -import { BadRequest400 } from './errors' - -export type Success = Result -export type Failure = Result -export class Result { - protected constructor( - readonly success: boolean, - readonly value: T | string, - ) {} - - static succeed(value: T): Success { - return new Result(true, value) as Success - } - - static fail(message: string): Failure { - return new Result(false, message) as Failure - } - - get isSuccess(): boolean { - return this.success - } - - get isError(): boolean { - return !this.success - } - - get data(): T { - if (this.success) return this.value as T - throw new Error('Cannot get data from a Failure') - } - - get error(): string { - if (!this.success) return this.value as string - throw new Error('Cannot get error from a Success') - } -} - -export function validateSchema(schemaValidation: SharedSafeParseReturnType) { - if (!schemaValidation.success) return new BadRequest400(parseZodError(schemaValidation.error)) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts deleted file mode 100644 index 7bf637c9e..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/controller.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { Cluster, Prisma, Project, ProjectMembers, ProjectRole } from '@prisma/client' -import type { XOR } from '@cpn-console/shared' -import { PROJECT_PERMS as PP, PROJECT_PERMS, projectIsLockedInfo, tokenHeaderName } from '@cpn-console/shared' -import type { FastifyRequest } from 'fastify' -import { Unauthorized401 } from './errors' -import { uuid } from './queries-tools' -import type { UserDetails } from '@old-server/types/index' -import prisma from '@old-server/prisma' -import { logViaSession, logViaToken } from '@old-server/resources/user/business' - -export type RequireOnlyOne = - Pick> - & { - [K in Keys]-?: - Required> - & Partial, undefined>> - }[Keys] - -type ErrorMessagePredicate = () => string | undefined -export function getErrorMessage(...fns: ErrorMessagePredicate[]) { - for (const f of fns) { - const error = f() - if (error) { - return error - } - } -} - -/** - * Renvoie une erreur si le projet est verrouillé - */ -export function checkProjectLocked(project: { locked: boolean }): string { - return project.locked - ? projectIsLockedInfo - : '' -} - -export function checkLocked(project: { locked: Project['locked'] }): string { - return checkProjectLocked(project) -} - -export function checkClusterUnavailable(clusterId: Cluster['id'], authorizedClusterIds: Cluster['id'][]): string { - return authorizedClusterIds.includes(clusterId) - ? '' - : 'Ce cluster n\'est pas disponible pour cette combinaison projet et stage' -} - -export const splitStringsFilterArray = >(toMatch: T, inputs: string): T => inputs.split(',').filter(i => toMatch.includes(i)) as unknown as T - -type StringArray = string[] -interface WhereBuilderParams { - enumValues: T - eqValue: T[number] | undefined - inValues: string | undefined - notInValues: string | undefined -} - -export function whereBuilder({ enumValues, eqValue, inValues, notInValues }: WhereBuilderParams) { - if (eqValue) { - return eqValue - } else if (inValues) { - return { in: splitStringsFilterArray(enumValues, inValues) } - } else if (notInValues) { - return { notIn: splitStringsFilterArray(enumValues, notInValues) } - } -} - -type ProjectMinimalPerms = Pick & { roles: ProjectRole[], members: ProjectMembers[] } -export interface UserProfile { user?: UserDetails, adminPermissions: bigint, tokenId?: string } -export interface ProjectPermState { projectPermissions?: bigint, projectId: Project['id'], projectLocked: boolean, projectStatus: Project['status'], projectOwnerId: Project['ownerId'] } -export type UserProjectProfile = UserProfile & ProjectPermState - -type ProjectUniqueFinder = XOR< - { slug: string }, - XOR<{ environmentId: string }, XOR<{ repositoryId: string }, { id: string }>> -> - -const projectPermsSelect = { roles: true, members: true, everyonePerms: true, ownerId: true, id: true, locked: true, status: true } as const satisfies Prisma.ProjectSelect - -export async function authUser(req: FastifyRequest): Promise -export async function authUser(req: FastifyRequest, projectUnique: ProjectUniqueFinder): Promise -export async function authUser(req: FastifyRequest, projectUnique?: ProjectUniqueFinder): Promise { - let adminPermissions: bigint = 0n - let tokenId: string | undefined - let user: UserDetails | undefined - - if (req.session.user) { - const loginResult = await logViaSession(req.session.user) - user = { - ...loginResult.user, - groups: req.session.user.groups, - } - adminPermissions = loginResult.adminPerms - } else { - const tokenHeader = req.headers[tokenHeaderName] - if (typeof tokenHeader === 'string') { - const resultToken = await logViaToken(tokenHeader) - if (typeof resultToken === 'string') { - throw new Unauthorized401(resultToken) - } - adminPermissions = resultToken.adminPerms ?? 0n - tokenId = resultToken.user.tokenId - if (!user && resultToken.user) { - user = { ...resultToken.user, groups: [] } - } - } - } - - const baseReturnInfos = { - user, - adminPermissions, - tokenId, - } - if (!projectUnique || !user) { - return baseReturnInfos - } - let project: ProjectMinimalPerms | null | undefined - - if (projectUnique.repositoryId) { - project = (await prisma.repository.findUnique({ - where: { id: projectUnique.repositoryId }, - select: { project: { select: projectPermsSelect } }, - }))?.project - } else if (projectUnique.environmentId) { - project = (await prisma.environment.findUnique({ - where: { id: projectUnique.environmentId }, - select: { project: { select: projectPermsSelect } }, - }))?.project - } else if (projectUnique.id) { - project = uuid.test(projectUnique.id) - ? await prisma.project.findUnique({ - where: { id: projectUnique.id }, - select: projectPermsSelect, - }) - : await prisma.project.findUnique({ - where: { slug: projectUnique.id }, - select: projectPermsSelect, - }) - } else if (projectUnique.slug) { - project = await prisma.project.findFirstOrThrow({ - where: { slug: projectUnique.slug }, - select: projectPermsSelect, - }) - } - if (!project) { - return baseReturnInfos - } - - const projectPermissions = getProjectPermissions(project, user) - - return { - user, - adminPermissions, - projectPermissions, - projectId: project.id, - projectLocked: project.locked, - projectStatus: project.status, - projectOwnerId: project.ownerId, - } -} - -function getProjectPermissions(project: ProjectMinimalPerms, user: UserDetails): bigint | undefined { - if (project.ownerId === user.id) return PP.MANAGE - const member = project.members.find(member => member.userId === user.id) - if (!member) return - - const memberRoles = project.roles.filter(role => member.roleIds.includes(role.id)) - return memberRoles.reduce((acc, curr) => acc | curr.permissions, project.everyonePerms | PROJECT_PERMS.GUEST) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts deleted file mode 100644 index 7cfd6c987..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getJSDateFromUtcIso } from './date' - -describe('date-util', () => { - it('should return a native Date object', () => { - const date = '2022-10-11' - - const received = getJSDateFromUtcIso(date) - - expect(received.getMonth()).toBe(9) - expect(received.getFullYear()).toBe(2022) - expect(received.getDate()).toBeGreaterThan(10) - expect(received.getDate()).toBeLessThan(12) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts deleted file mode 100644 index 87473d262..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/date.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { parseISO } from 'date-fns' - -export function getJSDateFromUtcIso(dateUtcIso: string) { - return parseISO(dateUtcIso) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts deleted file mode 100644 index 1a5caf387..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/env.ts +++ /dev/null @@ -1,57 +0,0 @@ -// import * as dotenv from 'dotenv' - -// if (process.env.DOCKER !== 'true') { - // dotenv.config({ path: '.env' }) -// } - -// if (process.env.INTEGRATION === 'true') { - // const envInteg = dotenv.config({ path: '.env.integ' }) - // process.env = { - // ...process.env, - // ...(envInteg?.parsed ?? {}), - // } -// } - -// // application mode -// export const isDev = process.env.NODE_ENV === 'development' -// export const isTest = process.env.NODE_ENV === 'test' -// export const isProd = process.env.NODE_ENV === 'production' -// export const isInt = process.env.INTEGRATION === 'true' -// export const isCI = process.env.CI === 'true' -// export const isDevSetup = process.env.DEV_SETUP === 'true' - -// // app -// export const port = process.env.SERVER_PORT -// export const appVersion = isProd - // ? (process.env.APP_VERSION ?? 'unknown') - // : 'dev' - -// // db -// export const dbUrl = process.env.DB_URL - -// // keycloak -// export const sessionSecret = process.env.SESSION_SECRET -// export const keycloakProtocol = process.env.KEYCLOAK_PROTOCOL -// export const keycloakDomain = process.env.KEYCLOAK_DOMAIN -// export const keycloakRealm = process.env.KEYCLOAK_REALM -// export const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID -// export const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET -// export const keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI -// export const adminsUserId = process.env.ADMIN_KC_USER_ID - // ? process.env.ADMIN_KC_USER_ID.split(',') - // : [] - -// export const contactEmail = process.env.CONTACT_EMAIL ?? 'cloudpinative-relations@interieur.gouv.fr' - -// // plugins -// export const mockPlugins = process.env.MOCK_PLUGINS === 'true' -// export const projectRootDir = process.env.PROJECTS_ROOT_DIR -// export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins' -// export const NODE_ENV = process.env.NODE_ENV === 'test' - // ? 'test' - // : process.env.NODE_ENV === 'development' - // ? 'development' - // : 'production' - -// // server tuning -// export const parallelBulkLimit = process.env.PARALLEL_BULK_LIMIT ? Number(process.env.PARALLEL_BULK_LIMIT) : 5 diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts deleted file mode 100644 index 0f1dd07fb..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/errors.ts +++ /dev/null @@ -1,48 +0,0 @@ -export class ErrorResType { - readonly status: 400 | 401 | 403 | 404 | 422 | 500 - body: { message: string } = { message: '' } - constructor(code: 400 | 401 | 403 | 404 | 422 | 500) { - this.status = code - } -} -export class BadRequest400 extends ErrorResType { - constructor(message: string) { - super(400) - this.body.message = message ?? 'Bad Request' - } -} - -export class Unauthorized401 extends ErrorResType { - constructor(message?: string) { - super(401) - this.body.message = message ?? 'Unauthorized' - } -} - -export class Forbidden403 extends ErrorResType { - constructor(message?: string) { - super(403) - this.body.message = message ?? 'Forbidden' - } -} - -export class NotFound404 extends ErrorResType { - constructor() { - super(404) - this.body.message = 'Not Found' - } -} - -export class Unprocessable422 extends ErrorResType { - constructor(message?: string) { - super(422) - this.body.message = message ?? 'Unprocessable Entity' - } -} - -export class Internal500 extends ErrorResType { - constructor(message: string) { - super(500) - this.body.message = message - } -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts deleted file mode 100644 index f3e0af579..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/fastify.ts +++ /dev/null @@ -1,55 +0,0 @@ -// import { randomUUID } from 'node:crypto' -// import type { FastifyServerOptions } from 'fastify' -// import type { generateOpenApi } from '@ts-rest/open-api' -// import { swaggerUiPath } from '@cpn-console/shared' -// import { loggerConf } from './logger' -// import { - // NODE_ENV, - // appVersion, - // keycloakClientId, - // keycloakClientSecret, - // keycloakRealm, - // keycloakRedirectUri, -// } from './env' -// import type { FastifySwaggerUiOptions } from '@fastify/swagger-ui' - -// export const fastifyConf: FastifyServerOptions = { - // maxParamLength: 5000, - // logger: loggerConf[NODE_ENV] ?? loggerConf.production, - // genReqId: () => randomUUID(), -// } - -// const externalDocs = { - // description: 'External documentation.', - // url: 'https://cloud-pi-native.fr', -// } - -// export const swaggerConf: Parameters[1] = { - // info: { - // title: 'Console Cloud Pi Native', - // description: 'API de gestion des ressources Cloud Pi Native.', - // version: appVersion, - // }, - - // externalDocs, - // servers: [ - // { - // url: keycloakRedirectUri, - // }, - // ], -// } - -// export const swaggerUiConf: FastifySwaggerUiOptions = { - // routePrefix: swaggerUiPath, - // uiConfig: { - // docExpansion: 'list', - // deepLinking: false, - // }, - // initOAuth: { - // clientId: keycloakClientId, - // clientSecret: keycloakClientSecret, - // realm: keycloakRealm, - // appName: 'Cloud Pi Native', - // scopes: 'openid generic', - // }, -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts deleted file mode 100644 index a31fffb0d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.spec.ts +++ /dev/null @@ -1,235 +0,0 @@ -import type { KubeCluster, KubeUser, Project as ProjectPayload, Store } from '@cpn-console/hooks' -import { describe, expect, it } from 'vitest' -import type { ProjectInfos, ReposCreds } from './hook-wrapper' -import { transformToHookProject } from './hook-wrapper' - -const associatedCluster = { - id: 'f0e39981-0b6d-4c16-aa96-225062b75767', - infos: '', - label: 'carno', - privacy: 'dedicated', - secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9b60', - kubeconfig: { - id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1437', - user: { - keyData: 'REDACTED', - certData: 'REDACTED', - }, - cluster: { - caData: 'REDACTED', - server: 'https://api-server:6443', - skipTLSVerify: false, - tlsServerName: 'api-server', - }, - createdAt: '2024-05-02T09:17:27.882Z', - updatedAt: '2024-05-02T09:17:27.882Z', - }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', - slug: 'default', - }, -} -const nonAssociatedCluster = { - id: 'f0e39981-0b6d-4c16-aa96-225062b75111', - infos: '', - label: 'carno2', - privacy: 'dedicated', - secretName: '4a38422c-29e1-4b61-b533-edaa1b8a9111', - kubeconfig: { - id: 'c8ba6db2-9a1d-4d6b-8b5e-2902cecd1111', - user: { - keyData: 'REDACTED', - certData: 'REDACTED', - }, - cluster: { - caData: 'REDACTED', - server: 'https://api-server:6443', - skipTLSVerify: false, - tlsServerName: 'api-server', - }, - createdAt: '2024-05-02T09:17:27.882Z', - updatedAt: '2024-05-02T09:17:27.882Z', - }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce0', - slug: 'default', - }, -} -const project: ProjectInfos = { - id: '011e7860-04d7-461f-912d-334c622d38b3', - name: 'candilib', - description: 'Application de réservation de places à l\'examen du permis B.', - status: 'created', - locked: false, - createdAt: '2023-07-03T14:46:56.778Z', - updatedAt: '2023-07-03T14:46:56.783Z', - everyonePerms: 896n, - ownerId: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', - members: [], - clusters: [associatedCluster, nonAssociatedCluster], - environments: [ - { - id: '1b9f1053-fcf5-4053-a7b2-ff8a2c0c1921', - name: 'dev', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - createdAt: '2023-07-03T14:46:56.787Z', - updatedAt: '2023-07-03T14:46:56.803Z', - clusterId: 'aaaaaaaa-5b03-45d5-847b-149dec875680', - quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', - stageId: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', - quota: { - id: '5a57b62f-2465-4fb6-a853-5a751d099199', - memory: '4Gi', - cpu: 2, - name: 'small', - isPrivate: false, - }, - stage: { - id: '4a9ad694-4c54-4a3c-9579-548bf4b7b1b9', - name: 'dev', - }, - cluster: { - id: 'aaaaaaaa-5b03-45d5-847b-149dec875680', - infos: 'Floating IP : 0.0.0.0', - label: 'pas-top-cluster', - privacy: 'dedicated', - secretName: '94d52618-7869-4192-b33e-85dd0959e815', - kubeconfig: { - id: 'b5662039-a62b-483e-ba54-b12c6f966c96', - user: { - token: 'kirikou', - }, - cluster: { - server: 'https://pwned.cluster', - tlsServerName: 'pwned.cluster', - }, - createdAt: '2024-07-24T16:54:14.969Z', - updatedAt: '2024-07-24T16:54:14.969Z', - }, - clusterResources: false, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', - slug: 'pub', - }, - }, - }, - { - id: '1c654f00-4798-4a80-929f-960ddb37885a', - name: 'integration', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - createdAt: '2023-07-03T14:46:56.788Z', - updatedAt: '2023-07-03T14:46:56.803Z', - clusterId: '126ac57f-263c-4463-87bb-d4e9017056b2', - quotaId: '5a57b62f-2465-4fb6-a853-5a751d099199', - stageId: 'd434310e-7850-4d59-b47f-0772edf50582', - quota: { - id: '5a57b62f-2465-4fb6-a853-5a751d099199', - memory: '4Gi', - cpu: 2, - name: 'small', - isPrivate: false, - }, - stage: { - id: 'd434310e-7850-4d59-b47f-0772edf50582', - name: 'integration', - }, - cluster: { - id: '126ac57f-263c-4463-87bb-d4e9017056b2', - infos: null, - label: 'top-secret-cluster', - privacy: 'dedicated', - secretName: '59be2d50-58f9-42f3-95dc-b0c0518e3d8a', - kubeconfig: { - id: '0e88f000-07e6-4781-a69d-0963489387f7', - user: { - token: 'nyan cat', - }, - cluster: { - server: 'https://nothere.cluster', - skipTLSVerify: false, - tlsServerName: 'nothere.cluster', - }, - createdAt: '2024-07-24T16:54:14.966Z', - updatedAt: '2024-07-24T16:54:14.966Z', - }, - clusterResources: true, - zone: { - id: 'a66c4230-eba6-41f1-aae5-bb1e4f90cce2', - slug: 'pub', - }, - }, - }, - ], - repositories: [ - { - id: '299216bb-2dcc-42b5-ac71-6aa001d2dccf', - projectId: '011e7860-04d7-461f-912d-334c622d38b3', - internalRepoName: 'candilib', - externalRepoUrl: 'https://github.com/dnum-mi/candilib.git', - externalUserName: 'this-is-a-test', - isInfra: false, - isPrivate: true, - createdAt: '2023-07-03T14:46:56.788Z', - updatedAt: '2023-07-03T14:46:56.802Z', - }, - ], - plugins: [], - owner: { - id: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', - firstName: 'Jean', - lastName: 'DUPOND', - email: 'test@test.com', - createdAt: '2023-07-03T14:46:56.770Z', - updatedAt: '2023-07-03T14:46:56.770Z', - adminRoleIds: [], - }, - roles: [], -} - -describe('transformToHookProject', () => { - // Mock data - const mockStore: Store = {} - const mockReposCreds: ReposCreds = { - console: { - token: 'test', - username: 'test', - }, - } - - it('transforme correctement le projet en objet Payload', () => { - const result: ProjectPayload = transformToHookProject(project, mockStore, mockReposCreds) - - // Asserts pour vérifier la transformation - - // Assert sur la transformation des utilisateurs - expect(result.users).toEqual([project.owner]) - - // Assert sur la transformation des rôles - expect(result.roles).toEqual([{ userId: project.owner.id, role: 'owner' }]) - - // Assert sur la transformation des clusters - expect(result.clusters).toEqual([associatedCluster, nonAssociatedCluster].map(({ kubeconfig, ...cluster }) => ({ - user: kubeconfig.user as unknown as KubeUser, - cluster: kubeconfig.cluster as unknown as KubeCluster, - ...cluster, - privacy: cluster.privacy, - }))) - - // Assert sur la transformation des environnements - expect(result.environments).toEqual(project.environments.map(({ permissions: _, stage, quota, ...environment }) => ({ - quota, - stage: stage.name, - permissions: [{ permissions: { rw: true, ro: true }, userId: project.ownerId }], - ...environment, - apis: {}, - }))) - - // Assert sur la transformation des repositories - expect(result.repositories).toEqual(project.repositories.map(repo => ({ ...repo, newCreds: mockReposCreds[repo.internalRepoName] }))) - - // Assert sur le store - expect(result.store).toEqual(mockStore) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts deleted file mode 100644 index 1754a82d2..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/hook-wrapper.ts +++ /dev/null @@ -1,231 +0,0 @@ -import type { Cluster, Kubeconfig, Project, ProjectRole, Zone } from '@prisma/client' -import type { ClusterObject, HookResult, KubeCluster, KubeUser, Project as ProjectPayload, RepoCreds, Repository, Store, ZoneObject } from '@cpn-console/hooks' -import { hooks } from '@cpn-console/hooks' -import type { AsyncReturnType } from '@cpn-console/shared' -import { ProjectAuthorized, getPermsByUserRoles, resourceListToDict } from '@cpn-console/shared' -import { genericProxy } from './proxy' -import { archiveProject, getAdminPlugin, getClusterByIdOrThrow, getClusterNamesByZoneId, getClustersAssociatedWithProject, getHookProjectInfos, getHookRepository, getProjectStore, getZoneByIdOrThrow, saveProjectStore, updateProjectClusterHistory, updateProjectCreated, updateProjectFailed, updateProjectWarning } from '@old-server/resources/queries-index' -import type { ConfigRecords } from '@old-server/resources/project-service/business' -import { dbToObj } from '@old-server/resources/project-service/business' - -export type ReposCreds = Record -export type ProjectInfos = AsyncReturnType - -async function getProjectPayload(projectId: Project['id'], reposCreds?: ReposCreds) { - const [ - project, - store, - clusters, - ] = await Promise.all([ - getHookProjectInfos(projectId), - getProjectStore(projectId), - getClustersAssociatedWithProject(projectId), - ]) - - return transformToHookProject({ - ...project, - clusters, - }, dbToObj(store), reposCreds) -} - -async function upsertProject(projectId: Project['id'], reposCreds?: ReposCreds) { - const [payload, config] = await Promise.all([ - getProjectPayload(projectId, reposCreds), - getAdminPlugin(), - ]) - - const results = await hooks.upsertProject.execute(payload, dbToObj(config)) - - const records: ConfigRecords = Object.entries(results.results).reduce((acc, [pluginName, result]) => { - if (result.store) { - return [...acc, ...Object.entries(result.store).map(([key, value]) => ({ pluginName, key, value: String(value) }))] - } - return acc - }, [] as ConfigRecords) - - await saveProjectStore(records, projectId) - const project = await manageProjectStatus(projectId, results, 'upsert', payload.environments.map(env => env.clusterId)) - return { - results, - project, - } -} -const project = { - upsert: async (projectId: Project['id'], reposCreds?: ReposCreds) => { - const results = await upsertProject(projectId, reposCreds) - // automatically retry one time if it fails - return results.results.failed ? upsertProject(projectId, reposCreds) : results - }, - delete: async (projectId: Project['id']) => { - const [payload, config] = await Promise.all([ - getProjectPayload(projectId), - getAdminPlugin(), - ]) - const results = await hooks.deleteProject.execute(payload, dbToObj(config)) - return { - results, - project: await manageProjectStatus(projectId, results, 'delete', []), - } - }, - getSecrets: async (projectId: Project['id']) => { - const project = await getHookProjectInfos(projectId) - const store = dbToObj(await getProjectStore(project.id)) - const config = dbToObj(await getAdminPlugin()) - - return hooks.getProjectSecrets.execute({ ...project, store }, config) - }, -} as const - -type ProjectAction = keyof typeof project -async function manageProjectStatus(projectId: Project['id'], hookReply: HookResult, action: ProjectAction, envClusterIds: Cluster['id'][]): Promise> { - if (!hookReply.failed && hookReply.results?.kubernetes) { - await updateProjectClusterHistory(projectId, envClusterIds) - } - if (hookReply.failed) { - return updateProjectFailed(projectId) - } else if (hookReply.warning.length) { - return updateProjectWarning(projectId) - } else if (action === 'upsert') { - return updateProjectCreated(projectId) - } else if (action === 'delete') { - return archiveProject(projectId) - } - throw new Error('unknown action') -} - -const cluster = { - upsert: async (clusterId: Cluster['id'], previousZoneId: Cluster['zoneId']) => { - const cluster = await getClusterByIdOrThrow(clusterId) - const clusterObject = cluster as unknown as ClusterObject - const store = dbToObj(await getAdminPlugin()) - if (cluster.zoneId !== previousZoneId) { - // Upsert on the old zone to remove cluster - const previousClusterObject = { - ...cluster, - } as unknown as ClusterObject - previousClusterObject.zone = await getZoneByIdOrThrow(previousZoneId) - previousClusterObject.zone.clusterNames = await getClusterNamesByZoneId(previousZoneId) - const hookResult = await hooks.upsertCluster.execute({ - ...cluster.kubeconfig as unknown as Pick, - ...previousClusterObject, - }, store) - if (hookResult.failed) { - return hookResult - } - } - clusterObject.zone.clusterNames = await getClusterNamesByZoneId(cluster.zoneId) - return hooks.upsertCluster.execute({ - ...cluster.kubeconfig as unknown as Pick, - ...clusterObject, - }, store) - }, - delete: async (clusterId: Cluster['id']) => { - const cluster = await getClusterByIdOrThrow(clusterId) - const clusterObject = cluster as unknown as ClusterObject - const clusterNames = await getClusterNamesByZoneId(cluster.zoneId) - clusterObject.zone.clusterNames = clusterNames.filter(c => c !== cluster.label) - const store = dbToObj(await getAdminPlugin()) - return hooks.deleteCluster.execute({ - ...cluster.kubeconfig as unknown as ClusterObject, - ...clusterObject, - }, store) - }, -} as const - -const user = { - retrieveUserByEmail: async (email: string) => { - const config = dbToObj(await getAdminPlugin()) - return hooks.retrieveUserByEmail.execute({ email }, config) - }, -} as const - -const zone = { - upsert: async (zoneId: Zone['id']) => { - const zone: ZoneObject = await getZoneByIdOrThrow(zoneId) - zone.clusterNames = await getClusterNamesByZoneId(zoneId) - const store = dbToObj(await getAdminPlugin()) - return hooks.upsertZone.execute(zone, store) - }, - delete: async (zoneId: Zone['id']) => { - const zone = await getZoneByIdOrThrow(zoneId) - const store = dbToObj(await getAdminPlugin()) - return hooks.deleteZone.execute(zone, store) - }, -} as const - -const misc = { - checkServices: async () => { - const config = dbToObj(await getAdminPlugin()) - return hooks.checkServices.execute({}, config) - }, - syncRepository: async (repoId: string, { syncAllBranches, branchName }: { syncAllBranches: boolean, branchName?: string }) => { - const { project, ...repoInfos } = await getHookRepository(repoId) - const store = dbToObj(await getProjectStore(project.id)) - const payload = { - repo: { ...repoInfos, syncAllBranches, branchName }, - ...project, - store, - } - const config = dbToObj(await getAdminPlugin()) - return hooks.syncRepository.execute(payload, config) - }, -} as const - -export const hook = { - // @ts-ignore TODO voir comment opti la signature de la fonction - misc: genericProxy(misc), - // @ts-ignore TODO voir comment opti la signature de la fonction - project: genericProxy(project, { upsert: ['delete'], delete: ['upsert', 'delete'], getSecrets: ['delete'] }), - // @ts-ignore TODO voir comment opti la signature de la fonction - cluster: genericProxy(cluster, { delete: ['upsert', 'delete'], upsert: ['delete'] }), - // @ts-ignore TODO voir comment opti la signature de la fonction - zone: genericProxy(zone, { delete: ['upsert'], upsert: ['delete'] }), - // @ts-ignore TODO voir comment opti la signature de la fonction - user: genericProxy(user, {}), -} - -function formatClusterInfos({ kubeconfig, ...cluster }: Omit - & { kubeconfig: Kubeconfig, zone: Pick }) { - return { - user: kubeconfig.user as unknown as KubeUser, - cluster: kubeconfig.cluster as unknown as KubeCluster, - ...cluster, - privacy: cluster.privacy, - } -} -export type RolesById = Record - -export function transformToHookProject(project: ProjectInfos, store: Store, reposCreds: ReposCreds = {}): ProjectPayload { - const clusters = project.clusters.map(cluster => formatClusterInfos(cluster)) - const rolesById = resourceListToDict(project.roles) - - return ({ - ...project, - clusters, - environments: project.environments.map(({ stage, ...environment }) => ({ - stage: stage.name, - permissions: [ - { permissions: { rw: true, ro: true }, userId: project.ownerId }, - ...project.members.map(member => ({ - userId: member.userId, - permissions: { - ro: ProjectAuthorized.ListEnvironments({ adminPermissions: 0n, projectPermissions: getPermsByUserRoles(member.roleIds, rolesById, project.everyonePerms) }), - rw: ProjectAuthorized.ManageEnvironments({ adminPermissions: 0n, projectPermissions: getPermsByUserRoles(member.roleIds, rolesById, project.everyonePerms) }), - }, - })), - ], - ...environment, - apis: {}, - })), - repositories: project.repositories.map(repo => ({ ...repo, newCreds: reposCreds[repo.internalRepoName] })), - store, - users: [project.owner, ...project.members.map(({ user }) => user)], - roles: [ - { userId: project.ownerId, role: 'owner' }, - ...project.members.map(member => ({ - userId: member.userId, - role: 'user' as const, - })), - ], - }) -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts deleted file mode 100644 index 5b7ce1b9c..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { userPayloadMapper } from './keycloak-utils' - -describe('keycloak', () => { - it('should map keycloak user object to DSO user object without groups', () => { - const payload = { - sub: 'thisIsAnId', - email: 'test@test.com', - given_name: 'Jean', - family_name: 'DUPOND', - } - const desired = { - id: 'thisIsAnId', - email: 'test@test.com', - firstName: 'Jean', - lastName: 'DUPOND', - groups: [], - } - - const transformed = userPayloadMapper(payload) - - expect(transformed).toMatchObject(desired) - }) - - it('should map keycloak user object to DSO user object with groups', () => { - const payload = { - sub: 'thisIsAnId', - email: 'test@test.com', - given_name: 'Jean', - family_name: 'DUPOND', - groups: ['group1'], - } - const desired = { - id: 'thisIsAnId', - email: 'test@test.com', - firstName: 'Jean', - lastName: 'DUPOND', - groups: ['group1'], - } - - const transformed = userPayloadMapper(payload) - - expect(transformed).toMatchObject(desired) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts deleted file mode 100644 index 029e9a2f5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak-utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -// import { tokenHeaderName } from '@cpn-console/shared' -// import type { FastifyRequest } from 'fastify' - -// interface KeycloakPayload { - // sub: string - // email: string - // given_name: string - // family_name: string - // groups: string[] -// } - -// export function userPayloadMapper(userPayload: KeycloakPayload) { - // return { - // id: userPayload.sub, - // email: userPayload.email, - // firstName: userPayload.given_name, - // lastName: userPayload.family_name, - // groups: userPayload.groups || [], - // } -// } - -// export function bypassFn(request: FastifyRequest) { - // try { - // return !!request.headers[tokenHeaderName] - // } catch (_e) {} - // return false -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts deleted file mode 100644 index a91fdde5f..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/keycloak.ts +++ /dev/null @@ -1,42 +0,0 @@ -// import { serviceContract, swaggerUiPath, systemContract } from '@cpn-console/shared' -// import type { KeycloakOptions } from 'fastify-keycloak-adapter' -// import { - // keycloakClientId, - // keycloakClientSecret, - // keycloakDomain, - // keycloakProtocol, - // keycloakRealm, - // keycloakRedirectUri, - // sessionSecret, -// } from './env' -// import { bypassFn, userPayloadMapper } from './keycloak-utils' - -// export const keycloakConf = { - // appOrigin: keycloakRedirectUri ?? 'http://localhost:8080', - // keycloakSubdomain: `${keycloakDomain}/realms/${keycloakRealm}`, - // clientId: keycloakClientId ?? '', - // clientSecret: keycloakClientSecret ?? '', - // useHttps: keycloakProtocol === 'https', - // disableCookiePlugin: true, - // disableSessionPlugin: true, - // // @ts-ignore - // userPayloadMapper, - // retries: 5, - // excludedPatterns: [ - // systemContract.getVersion.path, - // systemContract.getHealth.path, - // serviceContract.getServiceHealth.path, - // `${swaggerUiPath}/**`, - // ], - // bypassFn, -// } as const satisfies KeycloakOptions - -// export const sessionConf = { - // cookieName: 'sessionId', - // secret: sessionSecret || 'a-very-strong-secret-with-more-than-32-char', - // cookie: { - // httpOnly: true, - // secure: true, - // }, - // expires: 1_800_000, -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts deleted file mode 100644 index f6ded45b5..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/logger.ts +++ /dev/null @@ -1,97 +0,0 @@ -// import type { FastifyBaseLogger, FastifyLogFn, PinoLoggerOptions } from 'fastify/types/logger' -// import type { XOR } from '@cpn-console/shared' -// import { logger as customLogger } from '@old-server/app' - -// export const customLevels = { - // audit: 25, -// } - -// export const loggerConf: Record = { - // development: { - // transport: { - // target: 'pino-pretty', - // options: { - // translateTime: 'dd/mm/yyyy - HH:MM:ss Z', - // ignore: 'pid,hostname', - // colorize: true, - // singleLine: true, - // }, - // }, - // customLevels, - // level: process.env.LOG_LEVEL ?? 'debug', - // }, - // production: { - // customLevels, - // level: process.env.LOG_LEVEL ?? 'audit', - // }, - // test: { - // level: 'silent', - // }, -// } - -// type LoggerType = 'info' | 'warn' | 'error' | 'fatal' | 'trace' | 'debug' | 'audit' | undefined -// const loggerWrapper = { - // level: '', - // child: () => loggerWrapper, - // silent: () => {}, - // audit: (msg: string | unknown) => console.log(msg), - // info: (msg: string | unknown) => console.log(msg), - // warn: (msg: string | unknown) => console.warn(msg), - // error: (msg: string | unknown) => console.error(msg), - // fatal: (msg: string | unknown) => console.error(msg), - // trace: (msg: string | unknown) => console.trace(msg), - // debug: (msg: string | unknown) => console.debug(msg), -// } - -// export function log( - // type: LoggerType, - // { - // reqId, - // userId, - // tokenId, - // message, - // error, - // infos, - // }: { - // reqId?: string - // userId?: string - // tokenId?: string - // infos?: Record - // } & XOR<{ message: string }, { error: Record | string | Error }>, -// ) { - // const logger = customLogger || loggerWrapper - - // const logInfos = { - // message, - // infos, - // reqId, - // userId, - // tokenId, - // } - - // if (error) { - // const errorInfos = { - // ...logInfos, - // error: { - // message: typeof error === 'string' ? error : error?.message || 'unexpected error', - // trace: error instanceof Error && error?.stack, - // }, - // } - // logger.error({ ...errorInfos }) - // return - // } - // logger[type || 'info']({ reqId, userId, logInfos }) -// } - -// export interface CustomLogger extends FastifyBaseLogger { - /** - * Log at `'audit'` level the given msg. If the first argument is an object, all its properties will be included in the JSON line. - * If more args follows `msg`, these will be used to format `msg` using `util.format`. - * - * @typeParam T: the interface of the object being serialized. Default is object. - * @param obj: object to be serialized - * @param msg: the log message to write - * @param ...args: format string values when `msg` is a format string - */ - // audit: FastifyLogFn -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts deleted file mode 100644 index f5a40f0be..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/mocks.ts +++ /dev/null @@ -1,152 +0,0 @@ -import fp from 'fastify-plugin' -import type { Repository } from '@prisma/client' -import type { PluginsManifests, RepoCreds, ServiceInfos } from '@cpn-console/hooks' -import { editStrippers, populatePluginManifests } from '@cpn-console/hooks' -import { DEFAULT, DISABLED, PROJECT_PERMS } from '@cpn-console/shared' -import { faker } from '@faker-js/faker' -import type { UserDetails } from '../types/index' -import type * as utilsController from '../utils/controller' - -let requestor: Requestor - -export function setRequestor(user: Requestor = getRandomRequestor()) { - requestor = user -} - -export function getRequestor() { - return requestor -} - -export async function mockSessionPlugin() { - const sessionPlugin = (app, opt, next) => { - app.addHook('onRequest', (req, res, next) => { - req.session = { user: getRequestor() } - next() - }) - next() - } - - return { default: fp(sessionPlugin) } -} - -export async function mockHooksPackage() { - const hookTemplate = { - execute: () => ({ - args: {}, - failed: false, - }), - validate: () => ({ - failed: false, - }), - } - - return { - editStrippers, - populatePluginManifests, - services: { - getStatus: () => [], - refreshStatus: async () => [], - }, - PluginApi: class { }, - servicesInfos: { - registry: { title: 'Harbor', name: 'registry', to: () => 'test' }, - plugin2: { title: 'Plugin2', name: 'plugin2', to: () => ({ to: 'test', title: 'Test' }) }, - plugin3: { title: 'Plugin3', name: 'plugin3', to: () => [{ to: 'test', title: 'Test' }] }, - plugin4: { title: 'Plugin4', name: 'plugin4', to: () => [{ to: 'test' }] }, - plugin5: { title: 'Plugin5', name: 'plugin5' }, - } as Record, - pluginsManifests: { - registry: { - title: 'Harbor', - global: [{ - kind: 'switch', - initialValue: DEFAULT, - key: 'test2', - permissions: { - admin: { read: true, write: true }, - user: { read: true, write: false }, - }, - title: 'Test2', - value: DEFAULT, - description: 'description', - }], - project: [{ - kind: 'switch', - key: 'test2', - permissions: { - admin: { read: true, write: true }, - user: { read: true, write: true }, - }, - title: 'Test', - value: DEFAULT, - initialValue: DISABLED, - }], - }, - } as PluginsManifests, - hooks: { - // projects - getProjectSecrets: { - execute: () => ({ - failed: false, - args: {}, - results: { - registry: { - secrets: { - token: 'myToken', - }, - status: { - failed: false, - }, - }, - }, - }), - }, - upsertProject: hookTemplate, - deleteProject: hookTemplate, - // clusters - upsertCluster: hookTemplate, - deleteCluster: hookTemplate, - // user - retrieveUserByEmail: hookTemplate, - }, - } -} - -export type ReposCreds = Record - -type Requestor = Partial -export function getRandomRequestor(user?: Requestor): Partial { - return { - id: user?.id ?? faker.string.uuid(), - email: user?.email ?? faker.internet.email(), - firstName: user?.firstName ?? faker.person.firstName(), - lastName: user?.lastName ?? faker.person.lastName(), - type: 'human', - ...user?.groups !== null && { groups: user?.groups ?? [] }, - } -} - -export function getUserMockInfos(isAdmin: boolean, user?: UserDetails): utilsController.UserProfile & utilsController.ProjectPermState -export function getUserMockInfos(isAdmin: boolean, user?: UserDetails, project?: utilsController.ProjectPermState): utilsController.UserProjectProfile & utilsController.ProjectPermState -export function getUserMockInfos(isAdmin: boolean, user = getRandomRequestor(), project?: utilsController.ProjectPermState): utilsController.UserProfile | utilsController.UserProjectProfile { - return { - adminPermissions: isAdmin ? 2n : 0n, - user: user as UserDetails, - ...project, - } -} - -export function getProjectMockInfos({ projectId, projectLocked, projectOwnerId, projectPermissions, projectStatus }: Partial): utilsController.ProjectPermState { - return { - projectId: projectId ?? faker.string.uuid(), - projectLocked: projectLocked ?? false, - projectOwnerId: projectOwnerId ?? faker.string.uuid(), - projectStatus: projectStatus ?? 'created', - projectPermissions: projectPermissions ?? PROJECT_PERMS.MANAGE, - } -} - -export const atDates = { - createdAt: new Date(), - updatedAt: new Date(), -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts deleted file mode 100644 index f2f566967..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/plugins.ts +++ /dev/null @@ -1,9 +0,0 @@ -// import type { PluginManagerOptions } from '@cpn-console/hooks' -// import { isCI, isInt, isProd } from './env' - -// export const pluginManagerOptions: PluginManagerOptions = { - // mockHooks: isCI || (!isProd && !isInt), - // mockMonitoring: isCI || (!isProd && !isInt), - // mockExternalServices: isCI || (!isProd && !isInt), - // startPlugins: (!isCI && isProd) || isInt, -// } diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts deleted file mode 100644 index 757246d06..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { genericProxy } from './proxy' - -// Création d'une cible de test -const target = { - async fetchData(id: string) { - return { id, data: 'Mocked data' } - }, - async otherMethod(id: string) { - return { id, data: 'Mocked data' } - }, -} - -describe('test calls without ID passed', () => { - // Test d'appel de méthode sans ID - it('calling method without ID', async () => { - const proxied = genericProxy(target) - const result = await proxied.fetchData() - expect(result).toEqual({ id: undefined, data: 'Mocked data' }) - }) - - // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie - it('test when no ID is provided', async () => { - // Création d'une cible de test - const target = { - async fetchData() { - return 'No ID provided' - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - // Appel à la méthode fetchData sans ID - const result = await proxied.fetchData() - - // Vérification que le résultat est correct - expect(result).toBe('No ID provided') - }) - - // Fonction de test asynchrone pour tester le cas où aucune ID n'est fournie avec une promesse en cours - it('test when no ID is provided with pending promise', async () => { - // Création d'une cible de test - const target = { - async fetchData() { - return new Promise(resolve => setTimeout(() => resolve('Pending result'), 100)) - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - // Appel à la méthode fetchData sans ID - const promise1 = proxied.fetchData() - const promise2 = proxied.fetchData() // Deuxième appel avant la résolution du premier - - // Attendre que la première promesse se résolve - const result1 = await promise1 - - // Vérification que le résultat de la première promesse est correct - expect(result1).toBe('Pending result') - - // Attendre que la deuxième promesse se résolve - const result2 = await promise2 - - // Vérification que le résultat de la deuxième promesse est correct - expect(result2).toBe('Pending result') - }) - // Test pour vérifier que l'erreur est levée lorsque args est fourni sans ID - it('test error when args provided without ID', async () => { - // Création d'une cible de test - const target = { - async fetchData(_id: string, _args: any) { - return 'No ID provided' - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - const args = { key: 'value' } - - // Appel de la fonction fetchData avec des arguments mais sans ID - await expect(proxied.fetchData(undefined, args)).rejects.toThrow('ID is required when args are provided') - }) -}) - -describe('test calls with ID passed', () => { - // Test d'appel de méthode avec ID - it('calling method with ID', async () => { - const proxied = genericProxy(target) - const result = await proxied.fetchData('123') - expect(result).toEqual({ id: '123', data: 'Mocked data' }) - }) - - // Test d'appel de méthode avec exclusion en cours - it('calling method with exclusion in progress', async () => { - const proxied = genericProxy(target, { fetchData: ['otherMethod'] }) - // Simuler une exécution en cours pour la méthode exclue - proxied.otherMethod('456') - - // Maintenant, tenter d'appeler fetchData pour le même ID devrait échouer - await expect(proxied.fetchData('456')).rejects.toThrow( - 'otherMethod in progress on 456, can\'t fetchData', - ) - }) - - // Fonction de test asynchrone pour tester le mélange des nextArgs - it('test mixing nextArgs from concurrent promises', async () => { - // Création d'une cible de test - const target = { - async fetchData(id: string, args?: object) { - return { id, args } - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - const promise1 = proxied.fetchData('123', { key1: 'value1' }) - // Appels successifs à fetchData avec différents arguments - const promise2 = proxied.fetchData('123', { key2: 'value2' }) - - // Promesse concurrente avec des nextArgs différents - const promise3 = proxied.fetchData('123', { key3: 'value3' }) - - // Attendre que les promesses se résolvent - const result1 = await promise1 - const result2 = await promise2 - const result3 = await promise3 - - // Vérification que les nextArgs de promise2 et promise3 ont été correctement mélangés - expect(result1.args).toEqual({ key1: 'value1' }) - expect(result2.args).toEqual({ key2: 'value2', key3: 'value3' }) - expect(result3.args).toEqual({ key2: 'value2', key3: 'value3' }) - }) - - it('test rejection of set attempt', () => { - // Création d'une cible de test - const target = { - async fetchData() { - return 'Mocked data' - }, - } - - // Création du proxy - const proxied = genericProxy(target) - - // Tentative de définir une nouvelle propriété sur le proxy - const setAttempt = () => { - proxied.fetchData = () => new Promise(resolve => resolve('illegal')) - } - - // Vérification que la tentative de set est rejetée - expect(setAttempt).toThrow(TypeError) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts deleted file mode 100644 index ef915a7d1..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/proxy.ts +++ /dev/null @@ -1,78 +0,0 @@ -// @ts-nocheck un enfer à typer, pour plus tard -type Tracker> = Record - nextArgs?: [string] -}>> | Promise - -type Target = Record Promise> -type Excludes = Partial>> | undefined -const toTarget = (target: T) => ({ tracker: {} as Tracker, methods: target }) - -// @ts-ignore -export function genericProxy(proxied: T, excludes: Excludes = {}): T { - return new Proxy(toTarget(proxied), { - get({ methods, tracker }, property: string) { - if (!(property in methods)) return - return async (...args) => { - const id = args[0] as string - - if (!id && args.length > 0) { - throw new Error('ID is required when args are provided') - } - - if (!id) { - if (tracker[property] instanceof Promise) { - return tracker[property] - } - const p = methods[property]() - if (p instanceof Promise) { - tracker[property] = p - p.then(() => { - delete tracker[property] - }) - } - return p - } - if (!tracker[property]) { - tracker[property] = {} - } - - for (const testExclude of excludes[property] ?? []) { - // @ts-ignore - if (tracker?.[testExclude]?.[id]?.currentExec) { - throw new Error(`${String(testExclude)} in progress on ${id}, can't ${String(property)}`) - } - } - - if (id in tracker[property]) { - if (args[1]) { - tracker[property][id].nextArgs = { - ...(tracker[property][id].nextArgs ?? {}), - ...args[1], - } - } - if (tracker[property][id].currentExec) { - return new Promise((resolve) => { - tracker[property][id].currentExec.then(() => { - resolve(tracker[property][id].currentExec ?? methods[property](id, tracker[property][id].nextArgs)) - }) - }) - } - } - const p = methods[property](...args) - tracker[property][id] = { - currentExec: p, - nextArgs: undefined, - } - tracker[property][id].currentExec = p - p.then(() => { - tracker[property][id].currentExec = undefined - }) - return p - } - }, - set() { - return false - }, - }) as T -} diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts deleted file mode 100644 index 80f9740fc..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { exclude } from '@cpn-console/shared' -import { filterObjectByKeys } from './queries-tools' - -describe('queries-tools', () => { - it('should return a filtered object (filterObjectByKeys)', () => { - const initial = { - id: 'thisIsAnId', - name: 'alsoKeepThisKey', - description: 'keepThisKey', - } - const desired = { - name: 'alsoKeepThisKey', - description: 'keepThisKey', - } - - const transformed = filterObjectByKeys(initial, ['name', 'description']) - - expect(transformed).toMatchObject(desired) - }) - - it('should return a filtered object (exclude)', () => { - const initial = { - id: 'thisIsAnId', - name: 'myProjectName', - environment: { - permissions: { - password: 'secret', - id: 'notSecret', - }, - }, - } - const desired = { - id: 'thisIsAnId', - name: 'myProjectName', - environment: { - permissions: { - id: 'notSecret', - }, - }, - } - - const transformed = exclude(initial, ['password']) - - expect(transformed).toMatchObject(desired) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts deleted file mode 100644 index 856ca277f..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/queries-tools.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const dbKeysExcluded = ['updatedAt', 'createdAt'] - -// TODO -// @ts-ignore supprimer cette fonction et utiliser des schémas zod où elle est utilisé -export function filterObjectByKeys(obj, keys) { - return Object.fromEntries( - Object.entries(obj)?.filter(([key, _value]) => keys.includes(key)), - ) -} - -export const uuid: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i diff --git a/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts b/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts deleted file mode 100644 index f754da343..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/src/utils/random.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { createRandomDbSetup } from '@cpn-console/test-utils' - -describe('random utils', () => { - // TODO - it.skip('should create a random db for tests', () => { - const db = createRandomDbSetup({ nbUsers: 3, nbRepo: 1, envs: ['dev', 'prod'] }) - expect(db).toEqual( - expect.objectContaining({ - stages: expect.arrayContaining([ - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - { - id: expect.any(String), - name: expect.any(String), - }, - ]), - quotas: expect.arrayContaining([ - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - { - id: expect.any(String), - name: expect.any(String), - memory: expect.any(String), - cpu: expect.any(Number), - isPrivate: expect.any(Boolean), - }, - ]), - project: expect.objectContaining({ - id: expect.any(String), - name: expect.any(String), - clusters: expect.arrayContaining([{ - caData: expect.any(String), - server: expect.any(String), - tlsServername: expect.any(String), - }]), - status: expect.any(String), - locked: expect.any(Boolean), - roles: expect.arrayContaining([ - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - { - userId: expect.any(String), - projectId: expect.any(String), - role: expect.any(String), - user: expect.objectContaining({ - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }), - }, - ]), - repositories: expect.any(Array), - environments: expect.arrayContaining([ - { - id: expect.any(String), - stageId: expect.any(String), - projectId: expect.any(String), - quotaId: expect.any(String), - status: expect.any(String), - permissions: expect.any(Array), - clusters: expect.any(Array), - }, - { - id: expect.any(String), - stageId: expect.any(String), - projectId: expect.any(String), - quotaId: expect.any(String), - status: expect.any(String), - permissions: expect.any(Array), - clusters: expect.any(Array), - }, - ]), - }), - users: expect.arrayContaining([ - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - { - id: expect.any(String), - email: expect.any(String), - firstName: expect.any(String), - lastName: expect.any(String), - }, - ]), - }), - ) - }) -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts.backup b/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts.backup deleted file mode 100644 index 12ad9d735..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/vite.config.ts.backup +++ /dev/null @@ -1,18 +0,0 @@ -/// -import { URL, fileURLToPath } from 'node:url' -import { defineConfig } from 'vite' - -export default defineConfig({ - plugins: [ - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - test: { - poolMatchGlobs: [ - ['**/resources/**/*.spec', 'forks'], - ], - }, -}) diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts.backup b/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts.backup deleted file mode 100644 index 596420f7d..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/vitest-init.ts.backup +++ /dev/null @@ -1,11 +0,0 @@ -process.env.ARGOCD_URL = 'https://argo-cd.readthedocs.io' -process.env.GITLAB_URL = 'https://gitlab.com' -process.env.HARBOR_URL = 'https://goharbor.io' -process.env.NEXUS_URL = 'https://sonatype.com/products/nexus-repository' -process.env.SONARQUBE_URL = 'https://www.sonarqube.org' -process.env.VAULT_URL = 'https://www.vaultproject.io' -process.env.PROJECTS_ROOT_DIR = 'forge-mi/projects' -process.env.KEYCLOAK_REDIRECT_URI = 'http://console.dso.local' -process.env.CONTACT_EMAIL = 'cloudpinative-relations@interieur.gouv.fr' -process.env.OPENCDS_URL = 'https://opencds.gouv.fr' -process.env.OPENCDS_API_TOKEN = 'test_token' diff --git a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts.backup b/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts.backup deleted file mode 100644 index c8d5ceaa7..000000000 --- a/apps/server-nestjs/src/cpin-module/old-server/vitest.config.ts.backup +++ /dev/null @@ -1,34 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { mergeConfig } from 'vite' -import { configDefaults, defineConfig } from 'vitest/config' -import viteConfig from './vite.config' - -export default mergeConfig( - viteConfig, - defineConfig({ - test: { - reporters: ['default', 'hanging-process'], - environment: 'node', - testTimeout: 2000, - coverage: { - provider: 'v8', - reporter: ['text', 'lcov'], - include: ['src/**'], - exclude: [ - '**/types', - '**/mocks', - '**/*.spec', - '**/*.d', - '**/*.vue', - '**/queries', - '**/mocks', - ], - }, - include: ['src/**/*.spec.{ts,js}'], - exclude: [...configDefaults.exclude, 'e2e/*'], - setupFiles: ['./vitest-init'], - root: fileURLToPath(new URL('./', import.meta.url)), - pool: 'forks', - }, - }), -) From a649f92a2626e942c1fc9384076a665b027f12a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 6 Jan 2026 10:23:36 +0100 Subject: [PATCH 31/33] chore(server-nestjs): use old-server eslint config The NestJS default one reveals A LOT of issues I don't have time to fix yet... --- apps/server-nestjs/eslint.config.mjs | 36 +---- ...application-initialization.service.spec.ts | 28 ++-- .../application-initialization.service.ts | 22 +-- .../database-initialization.service.spec.ts | 28 ++-- .../database-initialization/utils.spec.ts | 83 +++++----- .../database-initialization/utils.ts | 146 ++++++++++-------- .../plugin-management.service.spec.ts | 26 ++-- .../plugin-management.service.ts | 80 +--------- .../cpin-module/core/app/app.service.spec.ts | 26 ++-- .../src/cpin-module/core/app/app.service.ts | 12 +- .../src/cpin-module/core/core.module.ts | 11 +- .../core/fastify/fastify.service.spec.ts | 26 ++-- .../admin-role-router.service.spec.ts | 26 ++-- .../admin-role-router.service.ts | 2 +- .../admin-token-router.service.spec.ts | 26 ++-- .../admin-token-router.service.ts | 2 +- .../cluster-router.service.spec.ts | 26 ++-- .../cluster-router/cluster-router.service.ts | 12 +- .../environment-router.service.spec.ts | 28 ++-- .../environment-router.service.ts | 8 +- .../log-router/log-router.service.spec.ts | 26 ++-- .../router/log-router/log-router.service.ts | 2 +- .../project-member-router.service.spec.ts | 28 ++-- .../project-member-router.service.ts | 17 +- .../project-role-router.service.spec.ts | 28 ++-- .../project-role-router.service.ts | 11 +- .../project-router.service.spec.ts | 26 ++-- .../project-router/project-router.service.ts | 17 +- .../project-service-router.service.spec.ts | 28 ++-- .../project-service-router.service.ts | 8 +- .../repository-router.service.spec.ts | 26 ++-- .../repository-router.service.ts | 20 ++- .../core/router/router.service.spec.ts | 26 ++-- .../cpin-module/core/router/router.service.ts | 41 ++--- .../service-chain-router.service.spec.ts | 28 ++-- .../service-chain-router.service.ts | 2 +- .../service-monitor-router.service.spec.ts | 28 ++-- .../service-monitor-router.service.ts | 2 +- .../stage-router/stage-router.service.spec.ts | 26 ++-- .../stage-router/stage-router.service.ts | 2 +- .../system-config-router.service.spec.ts | 28 ++-- .../system-config-router.service.ts | 2 +- .../system-router.service.spec.ts | 26 ++-- .../system-router/system-router.service.ts | 4 +- .../system-settings-router.service.spec.ts | 28 ++-- .../system-settings-router.service.ts | 2 +- .../user-router/user-router.service.spec.ts | 26 ++-- .../router/user-router/user-router.service.ts | 2 +- .../user-tokens-router.service.spec.ts | 26 ++-- .../user-tokens-router.service.ts | 2 +- .../zone-router/zone-router.service.spec.ts | 26 ++-- .../router/zone-router/zone-router.service.ts | 11 +- .../configuration.service.spec.ts | 26 ++-- .../configuration/configuration.service.ts | 1 - .../database/database.service.spec.ts | 26 ++-- .../database/database.service.ts | 1 + .../http-client/http-client.service.spec.ts | 26 ++-- .../infrastructure/infrastructure.module.ts | 12 +- .../infrastructure/logger/logger.module.ts | 2 +- .../server/server.service.spec.ts | 26 ++-- .../infrastructure/server/server.service.ts | 3 +- apps/server-nestjs/src/prisma.ts | 6 +- apps/server-nestjs/tsconfig.json | 3 +- 63 files changed, 686 insertions(+), 675 deletions(-) diff --git a/apps/server-nestjs/eslint.config.mjs b/apps/server-nestjs/eslint.config.mjs index 4e9f8271c..5a664d2b5 100644 --- a/apps/server-nestjs/eslint.config.mjs +++ b/apps/server-nestjs/eslint.config.mjs @@ -1,35 +1,3 @@ -// @ts-check -import eslint from '@eslint/js'; -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; -import globals from 'globals'; -import tseslint from 'typescript-eslint'; +import eslintConfigBase from '@cpn-console/eslint-config' -export default tseslint.config( - { - ignores: ['eslint.config.mjs'], - }, - eslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - eslintPluginPrettierRecommended, - { - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - sourceType: 'commonjs', - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - { - rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn', - "prettier/prettier": ["error", { endOfLine: "auto" }], - }, - }, -); +export default eslintConfigBase diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts index aa3e1a714..3ef55fb0c 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ApplicationInitializationService } from './application-initialization.service'; -describe('ApplicationInitializationServiceService', () => { - let service: ApplicationInitializationService; +describe('applicationInitializationServiceService', () => { + let service: ApplicationInitializationService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ApplicationInitializationService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ApplicationInitializationService], + }).compile(); - service = module.get(ApplicationInitializationService); - }); + service = module.get( + ApplicationInitializationService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts index ff2b81de1..15f3d99b2 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.ts @@ -1,8 +1,8 @@ +import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; import { DatabaseService } from '@/cpin-module/infrastructure/database/database.service'; import { Injectable, Logger } from '@nestjs/common'; import { rm } from 'node:fs/promises'; import { resolve } from 'node:path'; -import { ConfigurationService } from 'src/cpin-module/infrastructure/configuration/configuration.service'; import { DatabaseInitializationService } from '../database-initialization/database-initialization.service'; import { PluginManagementService } from '../plugin-management/plugin-management.service'; @@ -11,7 +11,7 @@ import { PluginManagementService } from '../plugin-management/plugin-management. export class ApplicationInitializationService { private readonly logger = new Logger(ApplicationInitializationService.name); constructor( - private readonly config: ConfigurationService, + private readonly configurationService: ConfigurationService, private readonly pluginManagementService: PluginManagementService, private readonly databaseInitializationService: DatabaseInitializationService, private readonly databaseService: DatabaseService, @@ -51,11 +51,15 @@ export class ApplicationInitializationService { try { const dataPath = - this.config.isProd || this.config.isInt + this.configurationService.isProd || + this.configurationService.isInt ? './init/db/imports/data' : '@cpn-console/test-utils/src/imports/data'; await this.injectDataInDatabase(dataPath); - if (this.config.isProd && !this.config.isDevSetup) { + if ( + this.configurationService.isProd && + !this.configurationService.isDevSetup + ) { this.logger.log('Cleaning up imported data file...'); await rm(resolve(__dirname, dataPath)); this.logger.log(`Successfully deleted '${dataPath}'`); @@ -74,11 +78,11 @@ export class ApplicationInitializationService { } this.logger.debug({ - isDev: this.config.isDev, - isTest: this.config.isTest, - isCI: this.config.isCI, - isDevSetup: this.config.isDevSetup, - isProd: this.config.isProd, + isDev: this.configurationService.isDev, + isTest: this.configurationService.isTest, + isCI: this.configurationService.isCI, + isDevSetup: this.configurationService.isDevSetup, + isProd: this.configurationService.isProd, }); } diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts index 2be00355c..b736d5163 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { DatabaseInitializationService } from './database-initialization.service'; -describe('DatabaseInitializationService', () => { - let service: DatabaseInitializationService; +describe('databaseInitializationService', () => { + let service: DatabaseInitializationService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [DatabaseInitializationService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DatabaseInitializationService], + }).compile(); - service = module.get(DatabaseInitializationService); - }); + service = module.get( + DatabaseInitializationService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.spec.ts b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.spec.ts index 4377e07a1..50cce2c10 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.spec.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.spec.ts @@ -1,52 +1,53 @@ -import { describe, expect, it, vi } from 'vitest' -import prisma from '../../__mocks__/prisma' -import { modelKeys, moveBefore, resourceListToDict } from './utils' +import { describe, expect, it, vi } from 'vitest'; -vi.mock('fs', () => ({ writeFileSync: vi.fn() })) +import prisma from '../../__mocks__/prisma'; +import { modelKeys, moveBefore, resourceListToDict } from './utils'; + +vi.mock('fs', () => ({ writeFileSync: vi.fn() })); for (const modelKey of modelKeys) { - prisma[modelKey].findMany.mockResolvedValue([]) + prisma[modelKey].findMany.mockResolvedValue([]); } describe('test moveBefore', () => { - it('should be moved', () => { - const arr = ['a', 'b', 'c'] - const arrSorted = moveBefore(arr, 'c', 'b') - expect(arrSorted).toEqual(['a', 'c', 'b']) - - const arrSorted2 = moveBefore(arr, 'c', 'a') - expect(arrSorted2).toEqual(['c', 'a', 'b']) - }) - it('should not be moved', () => { - const arr = ['a', 'b', 'c'] - const arrSorted = moveBefore(arr, 'b', 'c') - expect(arrSorted).toEqual(false) - - const arrSorted2 = moveBefore(arr, 'a', 'c') - expect(arrSorted2).toEqual(false) - - const arrSorted3 = moveBefore(arr, 'c', 'c') - expect(arrSorted3).toEqual(false) - }) -}) + it('should be moved', () => { + const arr = ['a', 'b', 'c']; + const arrSorted = moveBefore(arr, 'c', 'b'); + expect(arrSorted).toEqual(['a', 'c', 'b']); + + const arrSorted2 = moveBefore(arr, 'c', 'a'); + expect(arrSorted2).toEqual(['c', 'a', 'b']); + }); + it('should not be moved', () => { + const arr = ['a', 'b', 'c']; + const arrSorted = moveBefore(arr, 'b', 'c'); + expect(arrSorted).toEqual(false); + + const arrSorted2 = moveBefore(arr, 'a', 'c'); + expect(arrSorted2).toEqual(false); + + const arrSorted3 = moveBefore(arr, 'c', 'c'); + expect(arrSorted3).toEqual(false); + }); +}); it('test resourceListToDict (by name)', () => { - const list = [ - { name: 'a', value: 1 }, - { name: 'b', value: 2 }, - { name: 'c', value: 3 }, - ] - const dict = resourceListToDict(list) - expect(dict).toEqual({ - a: { name: 'a', value: 1 }, - b: { name: 'b', value: 2 }, - c: { name: 'c', value: 3 }, - }) -}) + const list = [ + { name: 'a', value: 1 }, + { name: 'b', value: 2 }, + { name: 'c', value: 3 }, + ]; + const dict = resourceListToDict(list); + expect(dict).toEqual({ + a: { name: 'a', value: 1 }, + b: { name: 'b', value: 2 }, + c: { name: 'c', value: 3 }, + }); +}); it('stringify bigint', () => { - const list = { name: 'a', value: 1n } + const list = { name: 'a', value: 1n }; - const dict = JSON.stringify(list) + const dict = JSON.stringify(list); - expect(dict).toEqual('{"name":"a","value":"1n"}') -}) + expect(dict).toEqual('{"name":"a","value":"1n"}'); +}); diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.ts b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.ts index 95924751a..941a10fa3 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/utils.ts @@ -1,85 +1,107 @@ // @ts-nocheck -import { Prisma } from '@prisma/client' +import { Prisma } from '@prisma/client'; // eslint-disable-next-line no-extend-native BigInt.prototype.toJSON = function () { - return `${this.toString()}n` -} + return `${this.toString()}n`; +}; -export type ResourceByName = Record -export function resourceListToDict(resList: Array): ResourceByName { - return resList.reduce((acc, curr) => { - return { - ...acc, - [curr.name]: curr, - } - }, {} as ResourceByName) +export type ResourceByName< + T extends { + name: string; + }, +> = Record; +export function resourceListToDict( + resList: Array, +): ResourceByName { + return resList.reduce( + (acc, curr) => { + return { + ...acc, + [curr.name]: curr, + }; + }, + {} as ResourceByName, + ); } // @ts-ignore -const Models = resourceListToDict(Prisma.dmmf.datamodel.models) -let ModelsNames = Object.keys(Models) -let ModelsOrder = [...ModelsNames] +const Models = resourceListToDict(Prisma.dmmf.datamodel.models); +let ModelsNames = Object.keys(Models); +let ModelsOrder = [...ModelsNames]; -export function moveBefore(arr: T, toMove: T[number], ref: T[number]): T | false { - const iref = arr.indexOf(ref) - const moveref = arr.indexOf(toMove) - if (moveref <= iref) return false - return [ - ...arr.slice(0, iref), - arr[moveref], - ...arr.slice(iref, moveref), - ...arr.slice(moveref + 1), - ] as T +export function moveBefore( + arr: T, + toMove: T[number], + ref: T[number], +): T | false { + const iref = arr.indexOf(ref); + const moveref = arr.indexOf(toMove); + if (moveref <= iref) return false; + return [ + ...arr.slice(0, iref), + arr[moveref], + ...arr.slice(iref, moveref), + ...arr.slice(moveref + 1), + ] as T; } -export const manyToManyRelation: [string, string, string][] = [] +export const manyToManyRelation: [string, string, string][] = []; function sort() { - let hasChanged = false - for (const model of ModelsNames) { - for (const field of Models[model].fields) { - if (field.isId) Models[model].id = field.name - if (field.type in Models) { - const relationField = Models[field.type].fields.find(({ type }) => type === model) - if (!relationField) throw new Error('unable to find matching model') - if ( - (relationField.isRequired && field.isRequired && !relationField.isList) - || (relationField.isRequired && !field.isRequired) - ) { - const moveRes = moveBefore(ModelsOrder, model, field.type) - if (moveRes) { - hasChanged = true - ModelsOrder = moveRes - } - } - if ( - field.isList && relationField.isList - && !manyToManyRelation.find(test => - (test[0] === model && test[1] === field.type) || (test[0] === field.type && test[1] === model)) - ) { - manyToManyRelation.push([model, field.type, field.name]) + let hasChanged = false; + for (const model of ModelsNames) { + for (const field of Models[model].fields) { + if (field.isId) Models[model].id = field.name; + if (field.type in Models) { + const relationField = Models[field.type].fields.find( + ({ type }) => type === model, + ); + if (!relationField) + throw new Error('unable to find matching model'); + if ( + (relationField.isRequired && + field.isRequired && + !relationField.isList) || + (relationField.isRequired && !field.isRequired) + ) { + const moveRes = moveBefore(ModelsOrder, model, field.type); + if (moveRes) { + hasChanged = true; + ModelsOrder = moveRes; + } + } + if ( + field.isList && + relationField.isList && + !manyToManyRelation.find( + (test) => + (test[0] === model && test[1] === field.type) || + (test[0] === field.type && test[1] === model), + ) + ) { + manyToManyRelation.push([model, field.type, field.name]); + } + } } - } } - } - ModelsNames = ModelsOrder - if (hasChanged) sort() + ModelsNames = ModelsOrder; + if (hasChanged) sort(); } -sort() +sort(); // special case to study -const logUserCase = moveBefore(ModelsOrder, 'User', 'Log') +const logUserCase = moveBefore(ModelsOrder, 'User', 'Log'); if (logUserCase) { - ModelsOrder = logUserCase + ModelsOrder = logUserCase; } -const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log') +const logProjectCase = moveBefore(ModelsOrder, 'Project', 'Log'); if (logProjectCase) { - ModelsOrder = logProjectCase + ModelsOrder = logProjectCase; } -export const models: Record = {} -export const associations: Record = [] -export const modelKeys = ModelsOrder.map(model => model.slice(0, 1).toLocaleLowerCase() + model.slice(1)) +export const models: Record = {}; +export const associations: Record = []; +export const modelKeys = ModelsOrder.map( + (model) => model.slice(0, 1).toLocaleLowerCase() + model.slice(1), +); diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.spec.ts b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.spec.ts index a29b76f88..f299b1df6 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { PluginManagementService } from './plugin-management.service'; -describe('PluginManagementService', () => { - let service: PluginManagementService; +describe('pluginManagementService', () => { + let service: PluginManagementService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [PluginManagementService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PluginManagementService], + }).compile(); - service = module.get(PluginManagementService); - }); + service = module.get(PluginManagementService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts index 191130f6a..600b14e8f 100644 --- a/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts +++ b/apps/server-nestjs/src/cpin-module/application-initialization/plugin-management/plugin-management.service.ts @@ -1,84 +1,8 @@ -import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; -import { plugin as argo } from '@cpn-console/argocd-plugin'; -import { plugin as gitlab } from '@cpn-console/gitlab-plugin'; -import { plugin as harbor } from '@cpn-console/harbor-plugin'; -import { - type Plugin, - PluginManagerOptions, - pluginManager, -} from '@cpn-console/hooks'; -import { plugin as keycloak } from '@cpn-console/keycloak-plugin'; -import { plugin as kubernetes } from '@cpn-console/kubernetes-plugin'; -import { plugin as nexus } from '@cpn-console/nexus-plugin'; -import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin'; -import { plugin as vault } from '@cpn-console/vault-plugin'; import { Injectable } from '@nestjs/common'; -import { readdirSync, statSync } from 'node:fs'; @Injectable() export class PluginManagementService { - constructor(private readonly configurationService: ConfigurationService) {} + constructor() {} - async initPm() { - const pluginManagerOptions: PluginManagerOptions = { - mockHooks: - this.configurationService.isCI || - (!this.configurationService.isProd && - !this.configurationService.isInt), - mockMonitoring: - this.configurationService.isCI || - (!this.configurationService.isProd && - !this.configurationService.isInt), - mockExternalServices: - this.configurationService.isCI || - (!this.configurationService.isProd && - !this.configurationService.isInt), - startPlugins: - (!this.configurationService.isCI && - this.configurationService.isProd) || - this.configurationService.isInt, - }; - const pm = pluginManager(pluginManagerOptions); - pm.register(argo); - pm.register(gitlab); - pm.register(harbor); - pm.register(keycloak); - pm.register(kubernetes); - pm.register(nexus); - pm.register(sonarqube); - pm.register(vault); - - if ( - !statSync(this.configurationService.pluginsDir, { - throwIfNoEntry: false, - }) - ) { - return pm; - } - for (const dirName of readdirSync( - this.configurationService.pluginsDir, - )) { - const moduleAbsPath = `${this.configurationService.pluginsDir}/${dirName}`; - try { - statSync(`${moduleAbsPath}/package.json`); - const pkg = await import(`${moduleAbsPath}/package.json`, { - with: { type: 'json' }, - }); - const entrypoint = pkg.default.module || pkg.default.main; - if (!entrypoint) - throw new Error( - `No entrypoint found in package.json : ${pkg.default.name}`, - ); - const { plugin } = (await import( - `${moduleAbsPath}/${entrypoint}` - )) as { plugin: Plugin }; - pm.register(plugin); - } catch (error) { - console.error(`Could not import module ${moduleAbsPath}`); - console.error(error.stack); - } - } - - return pm; - } + async initPm() {} } diff --git a/apps/server-nestjs/src/cpin-module/core/app/app.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/app/app.service.spec.ts index 0d0025276..0106edf11 100644 --- a/apps/server-nestjs/src/cpin-module/core/app/app.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/app/app.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { AppService } from './app.service'; -describe('AppService', () => { - let service: AppService; +describe('appService', () => { + let service: AppService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AppService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AppService], + }).compile(); - service = module.get(AppService); - }); + service = module.get(AppService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts index 4533c1556..5ef1bb83c 100644 --- a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts @@ -1,24 +1,26 @@ import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; -import { apiPrefix, getContract } from '@cpn-console/shared'; import { + apiPrefix, + getContract, serviceContract, swaggerUiPath, systemContract, + tokenHeaderName, } from '@cpn-console/shared'; -import { tokenHeaderName } from '@cpn-console/shared'; import fastifyCookie from '@fastify/cookie'; import helmet from '@fastify/helmet'; -import fastifySession, { FastifySessionOptions } from '@fastify/session'; +import type { FastifySessionOptions } from '@fastify/session'; +import fastifySession from '@fastify/session'; import fastifySwagger from '@fastify/swagger'; import fastifySwaggerUi from '@fastify/swagger-ui'; import { Injectable, Logger } from '@nestjs/common'; import { generateOpenApi } from '@ts-rest/open-api'; import fastify from 'fastify'; import type { FastifyRequest } from 'fastify'; -import keycloak, { KeycloakOptions } from 'fastify-keycloak-adapter'; +import type { KeycloakOptions } from 'fastify-keycloak-adapter'; +import keycloak from 'fastify-keycloak-adapter'; import { FastifyService } from '../fastify/fastify.service'; -import { RouterService } from '../router/router.service'; interface KeycloakPayload { sub: string; diff --git a/apps/server-nestjs/src/cpin-module/core/core.module.ts b/apps/server-nestjs/src/cpin-module/core/core.module.ts index bdf1196bd..595bebd03 100644 --- a/apps/server-nestjs/src/cpin-module/core/core.module.ts +++ b/apps/server-nestjs/src/cpin-module/core/core.module.ts @@ -7,14 +7,7 @@ import { FastifyService } from './fastify/fastify.service'; import { RouterModule } from './router/router.module'; @Module({ - imports: [ - ConfigurationModule, - RouterModule, - InfrastructureModule, - ], - providers: [ - AppService, - FastifyService - ], + imports: [ConfigurationModule, RouterModule, InfrastructureModule], + providers: [AppService, FastifyService], }) export class CoreModule {} diff --git a/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.spec.ts index 6c473f5b1..cd9cab9bf 100644 --- a/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/fastify/fastify.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { FastifyService } from './fastify.service'; -describe('FastifyService', () => { - let service: FastifyService; +describe('fastifyService', () => { + let service: FastifyService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [FastifyService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [FastifyService], + }).compile(); - service = module.get(FastifyService); - }); + service = module.get(FastifyService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts index 4022968e3..f679cb58e 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { AdminRoleRouterService } from './admin-role-router.service'; -describe('AdminRoleRouterService', () => { - let service: AdminRoleRouterService; +describe('adminRoleRouterService', () => { + let service: AdminRoleRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AdminRoleRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminRoleRouterService], + }).compile(); - service = module.get(AdminRoleRouterService); - }); + service = module.get(AdminRoleRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts index 3a7117c93..b3901e909 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts index 2a759d155..f1a3927b4 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { AdminTokenRouterService } from './admin-token-router.service'; -describe('AdminTokenRouterService', () => { - let service: AdminTokenRouterService; +describe('adminTokenRouterService', () => { + let service: AdminTokenRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AdminTokenRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminTokenRouterService], + }).compile(); - service = module.get(AdminTokenRouterService); - }); + service = module.get(AdminTokenRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts index 3f365c71d..e664d03e8 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { diff --git a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts index 77b6d446a..dbb18f899 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ClusterRouterService } from './cluster-router.service'; -describe('ClusterRouterService', () => { - let service: ClusterRouterService; +describe('clusterRouterService', () => { + let service: ClusterRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ClusterRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ClusterRouterService], + }).compile(); - service = module.get(ClusterRouterService); - }); + service = module.get(ClusterRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts index 0e9a89bce..88b8cda34 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts @@ -1,3 +1,4 @@ +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import type { AsyncReturnType } from '@cpn-console/shared'; import { AdminAuthorized, clusterContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; @@ -18,8 +19,6 @@ import { Unauthorized401, } from '@old-server/utils/errors'; -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; - @Injectable() export class ClusterRouterService { constructor(private readonly serverService: ServerService) {} @@ -74,10 +73,11 @@ export class ClusterRouterService { if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403(); - if (!user) + if (!user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } const body = await createCluster(data, user.id, req.id); if (body instanceof ErrorResType) return body; @@ -106,10 +106,11 @@ export class ClusterRouterService { const { user, adminPermissions } = await authUser(req); if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403(); - if (!user) + if (!user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } const clusterId = params.clusterId; const body = await updateCluster( @@ -135,10 +136,11 @@ export class ClusterRouterService { const { user, adminPermissions, tokenId } = await authUser(req); if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403(); - if (!user?.id && !tokenId) + if (!user?.id && !tokenId) { return new Unauthorized401( 'Your identity has not been found', ); + } const clusterId = params.clusterId; const body = await deleteCluster({ diff --git a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts index 062359821..bd5c22e5e 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { EnvironmentRouterService } from './environment-router.service'; -describe('EnvironmentRouterService', () => { - let service: EnvironmentRouterService; +describe('environmentRouterService', () => { + let service: EnvironmentRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [EnvironmentRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EnvironmentRouterService], + }).compile(); - service = module.get(EnvironmentRouterService); - }); + service = module.get( + EnvironmentRouterService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts index b2757612d..59446c561 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { ProjectAuthorized, environmentContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { @@ -42,10 +42,11 @@ export class EnvironmentRouterService { const projectId = requestBody.projectId; const perms = await authUser(req, { id: projectId }); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } if (!perms.projectPermissions) return new NotFound404(); if (!ProjectAuthorized.ManageEnvironments(perms)) return new Forbidden403(); @@ -87,10 +88,11 @@ export class EnvironmentRouterService { }) => { const { environmentId } = params; const perms = await authUser(req, { environmentId }); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } if (!ProjectAuthorized.ListEnvironments(perms)) return new NotFound404(); if (!ProjectAuthorized.ManageEnvironments(perms)) diff --git a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts index 958c0af40..5d3fe7954 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { LogRouterService } from './log-router.service'; -describe('LogRouterService', () => { - let service: LogRouterService; +describe('logRouterService', () => { + let service: LogRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [LogRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LogRouterService], + }).compile(); - service = module.get(LogRouterService); - }); + service = module.get(LogRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts index 44e5934b5..5b45f936a 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import type { CleanLog, Log, XOR } from '@cpn-console/shared'; import { AdminAuthorized, logContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts index 63749a0dd..9ecff5701 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ProjectMemberRouterService } from './project-member-router.service'; -describe('ProjectMemberRouterService', () => { - let service: ProjectMemberRouterService; +describe('projectMemberRouterService', () => { + let service: ProjectMemberRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectMemberRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectMemberRouterService], + }).compile(); - service = module.get(ProjectMemberRouterService); - }); + service = module.get( + ProjectMemberRouterService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts index fecd58b7f..f1864e486 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, ProjectAuthorized, @@ -31,8 +31,9 @@ export class ProjectMemberRouterService { if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } const body = await listMembers(projectId); @@ -46,15 +47,17 @@ export class ProjectMemberRouterService { const { projectId } = params; const perms = await authUser(req, { id: projectId }); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } if (!ProjectAuthorized.ManageMembers(perms)) return new Forbidden403(); if (perms.projectLocked) @@ -109,14 +112,16 @@ export class ProjectMemberRouterService { if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } if ( !ProjectAuthorized.ManageMembers(perms) && userId !== perms.user?.id - ) + ) { return new Forbidden403(); + } const resBody = await removeMember(projectId, params.userId); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts index 1fb57b0bb..7ee22fb44 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ProjectRoleRouterService } from './project-role-router.service'; -describe('ProjectRoleRouterService', () => { - let service: ProjectRoleRouterService; +describe('projectRoleRouterService', () => { + let service: ProjectRoleRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectRoleRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectRoleRouterService], + }).compile(); - service = module.get(ProjectRoleRouterService); - }); + service = module.get( + ProjectRoleRouterService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts index 73c612192..7bc5b7df7 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, ProjectAuthorized, @@ -32,8 +32,9 @@ export class ProjectRoleRouterService { if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } const body = await listRoles(projectId); @@ -53,8 +54,9 @@ export class ProjectRoleRouterService { if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } if (!ProjectAuthorized.ManageRoles(perms)) return new Forbidden403(); if (perms.projectLocked) @@ -100,8 +102,9 @@ export class ProjectRoleRouterService { if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } const resBody = await countRolesMembers(projectId); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts index 8ac556d0f..3378547c2 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ProjectRouterService } from './project-router.service'; -describe('ProjectRouterService', () => { - let service: ProjectRouterService; +describe('projectRouterService', () => { + let service: ProjectRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectRouterService], + }).compile(); - service = module.get(ProjectRouterService); - }); + service = module.get(ProjectRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts index 34154d9e4..be1b5d376 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import type { AsyncReturnType } from '@cpn-console/shared'; import { AdminAuthorized, @@ -81,10 +81,11 @@ export class ProjectRouterService { // Créer un projet createProject: async ({ request: req, body: data }) => { const perms = await authUser(req); - if (perms.user?.type !== 'human') + if (perms.user?.type !== 'human') { return new Unauthorized401( 'Cannot find requestor in database', ); + } const body = await createProject(data, perms.user, req.id); if (body instanceof ErrorResType) return body; @@ -124,10 +125,11 @@ export class ProjectRouterService { const projectId = params.projectId; const perms = await authUser(req, { id: projectId }); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Cannot find requestor in database', ); + } const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); const isOwner = perms.projectOwnerId === perms.user.id; @@ -143,10 +145,11 @@ export class ProjectRouterService { if (perms.projectLocked) { if (!isAdmin) return new Forbidden403('Le projet est verrouillé'); - if (data.locked !== false) + if (data.locked !== false) { return new Forbidden403( 'Veuillez déverrouiler le projet pour le mettre à jour', ); + } } if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); @@ -196,10 +199,11 @@ export class ProjectRouterService { const perms = await authUser(req, { id: projectId }); const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Cannot find requestor in database', ); + } if (!perms.projectPermissions && !isAdmin) return new NotFound404(); if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); @@ -232,10 +236,11 @@ export class ProjectRouterService { bulkActionProject: async ({ request: req, body }) => { const perms = await authUser(req); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Cannot find requestor in database', ); + } if (!AdminAuthorized.isAdmin(perms.adminPermissions)) return new Forbidden403(); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts index 15b97798d..120837802 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ProjectServiceRouterService } from './project-service-router.service'; -describe('ProjectServiceRouterService', () => { - let service: ProjectServiceRouterService; +describe('projectServiceRouterService', () => { + let service: ProjectServiceRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectServiceRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProjectServiceRouterService], + }).compile(); - service = module.get(ProjectServiceRouterService); - }); + service = module.get( + ProjectServiceRouterService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts index 09d363d52..093555c35 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, ProjectAuthorized, @@ -30,15 +30,17 @@ export class ProjectServiceRouterService { if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } if ( !AdminAuthorized.isAdmin(perms.adminPermissions) && query.permissionTarget === 'admin' - ) + ) { return new Forbidden403( 'Vous ne pouvez pas demander les paramètres admin', ); + } const body = await getProjectServices( projectId, diff --git a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts index b27de5a4c..bbc128657 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { RepositoryRouterService } from './repository-router.service'; -describe('RepositoryRouterService', () => { - let service: RepositoryRouterService; +describe('repositoryRouterService', () => { + let service: RepositoryRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [RepositoryRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RepositoryRouterService], + }).compile(); - service = module.get(RepositoryRouterService); - }); + service = module.get(RepositoryRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts index d5acd1562..a703868e6 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, ProjectAuthorized, @@ -47,10 +47,11 @@ export class RepositoryRouterService { syncRepository: async ({ request: req, params, body }) => { const { repositoryId } = params; const perms = await authUser(req, { repositoryId }); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } if (!perms.projectPermissions) return new NotFound404(); if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403(); @@ -79,15 +80,17 @@ export class RepositoryRouterService { const projectId = data.projectId; const perms = await authUser(req, { id: projectId }); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403(); if (perms.projectLocked) @@ -113,15 +116,17 @@ export class RepositoryRouterService { const repositoryId = params.repositoryId; const perms = await authUser(req, { repositoryId }); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } if ( !perms.projectPermissions && !AdminAuthorized.isAdmin(perms.adminPermissions) - ) + ) { return new NotFound404(); + } if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403(); if (perms.projectLocked) @@ -169,10 +174,11 @@ export class RepositoryRouterService { const repositoryId = params.repositoryId; const perms = await authUser(req, { repositoryId }); - if (!perms.user) + if (!perms.user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } if (!perms.projectPermissions) return new NotFound404(); if (!ProjectAuthorized.ManageRepositories(perms)) return new Forbidden403(); diff --git a/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts index c35fac5d3..8311db3ba 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { RouterService } from './router.service'; -describe('RouterService', () => { - let service: RouterService; +describe('routerService', () => { + let service: RouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [RouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RouterService], + }).compile(); - service = module.get(RouterService); - }); + service = module.get(RouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/router.service.ts index 9b58fcb82..6204668d9 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/router.service.ts @@ -1,26 +1,26 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { Injectable } from '@nestjs/common'; import type { FastifyInstance } from 'fastify'; -import { AdminRoleRouterService } from './admin-role-router/admin-role-router.service'; -import { AdminTokenRouterService } from './admin-token-router/admin-token-router.service'; -import { ClusterRouterService } from './cluster-router/cluster-router.service'; -import { EnvironmentRouterService } from './environment-router/environment-router.service'; -import { LogRouterService } from './log-router/log-router.service'; -import { ProjectMemberRouterService } from './project-member-router/project-member-router.service'; -import { ProjectRoleRouterService } from './project-role-router/project-role-router.service'; -import { ProjectRouterService } from './project-router/project-router.service'; -import { ProjectServiceRouterService } from './project-service-router/project-service-router.service'; -import { RepositoryRouterService } from './repository-router/repository-router.service'; -import { ServiceChainRouterService } from './service-chain-router/service-chain-router.service'; -import { ServiceMonitorRouterService } from './service-monitor-router/service-monitor-router.service'; -import { StageRouterService } from './stage-router/stage-router.service'; -import { SystemConfigRouterService } from './system-config-router/system-config-router.service'; -import { SystemRouterService } from './system-router/system-router.service'; -import { SystemSettingsRouterService } from './system-settings-router/system-settings-router.service'; -import { UserRouterService } from './user-router/user-router.service'; -import { UserTokensRouterService } from './user-tokens-router/user-tokens-router.service'; -import { ZoneRouterService } from './zone-router/zone-router.service'; +import type { AdminRoleRouterService } from './admin-role-router/admin-role-router.service'; +import type { AdminTokenRouterService } from './admin-token-router/admin-token-router.service'; +import type { ClusterRouterService } from './cluster-router/cluster-router.service'; +import type { EnvironmentRouterService } from './environment-router/environment-router.service'; +import type { LogRouterService } from './log-router/log-router.service'; +import type { ProjectMemberRouterService } from './project-member-router/project-member-router.service'; +import type { ProjectRoleRouterService } from './project-role-router/project-role-router.service'; +import type { ProjectRouterService } from './project-router/project-router.service'; +import type { ProjectServiceRouterService } from './project-service-router/project-service-router.service'; +import type { RepositoryRouterService } from './repository-router/repository-router.service'; +import type { ServiceChainRouterService } from './service-chain-router/service-chain-router.service'; +import type { ServiceMonitorRouterService } from './service-monitor-router/service-monitor-router.service'; +import type { StageRouterService } from './stage-router/stage-router.service'; +import type { SystemConfigRouterService } from './system-config-router/system-config-router.service'; +import type { SystemRouterService } from './system-router/system-router.service'; +import type { SystemSettingsRouterService } from './system-settings-router/system-settings-router.service'; +import type { UserRouterService } from './user-router/user-router.service'; +import type { UserTokensRouterService } from './user-tokens-router/user-tokens-router.service'; +import type { ZoneRouterService } from './zone-router/zone-router.service'; @Injectable() export class RouterService { @@ -46,6 +46,7 @@ export class RouterService { private readonly userTokensRouterService: UserTokensRouterService, private readonly zoneRouterService: ZoneRouterService, ) {} + // relax validation schema if NO_VALIDATION env var is set to true. // /!\ It can lead to security leaks !!!! validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' }; diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts index 70902e4d4..096ae0428 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ServiceChainRouterService } from './service-chain-router.service'; -describe('ServiceChainRouterService', () => { - let service: ServiceChainRouterService; +describe('serviceChainRouterService', () => { + let service: ServiceChainRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ServiceChainRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ServiceChainRouterService], + }).compile(); - service = module.get(ServiceChainRouterService); - }); + service = module.get( + ServiceChainRouterService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts index 0b9cbab27..fe3f4692f 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import type { AsyncReturnType } from '@cpn-console/shared'; import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts index d1b53b2a6..28afbe393 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ServiceMonitorRouterService } from './service-monitor-router.service'; -describe('ServiceMonitorRouterService', () => { - let service: ServiceMonitorRouterService; +describe('serviceMonitorRouterService', () => { + let service: ServiceMonitorRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ServiceMonitorRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ServiceMonitorRouterService], + }).compile(); - service = module.get(ServiceMonitorRouterService); - }); + service = module.get( + ServiceMonitorRouterService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts index 7a9846feb..b1a4ebeb1 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, serviceContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { diff --git a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts index aec34bee9..2c4fbf095 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { StageRouterService } from './stage-router.service'; -describe('StageRouterService', () => { - let service: StageRouterService; +describe('stageRouterService', () => { + let service: StageRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [StageRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [StageRouterService], + }).compile(); - service = module.get(StageRouterService); - }); + service = module.get(StageRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts index 43f1ac65f..086056723 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, stageContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts index 4691aa309..976c92f68 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { SystemConfigRouterService } from './system-config-router.service'; -describe('SystemConfigRouterService', () => { - let service: SystemConfigRouterService; +describe('systemConfigRouterService', () => { + let service: SystemConfigRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [SystemConfigRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SystemConfigRouterService], + }).compile(); - service = module.get(SystemConfigRouterService); - }); + service = module.get( + SystemConfigRouterService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts index b0a0dd74b..387355e6f 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts index 7ddc7e4ee..d35f07f6c 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { SystemRouterService } from './system-router.service'; -describe('SystemRouterService', () => { - let service: SystemRouterService; +describe('systemRouterService', () => { + let service: SystemRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [SystemRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SystemRouterService], + }).compile(); - service = module.get(SystemRouterService); - }); + service = module.get(SystemRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts index 4d1cbaddf..483c2f300 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts @@ -1,5 +1,5 @@ -import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { systemContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts index be353261a..1fe094e4d 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts @@ -1,18 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { SystemSettingsRouterService } from './system-settings-router.service'; -describe('SystemSettingsRouterService', () => { - let service: SystemSettingsRouterService; +describe('systemSettingsRouterService', () => { + let service: SystemSettingsRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [SystemSettingsRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SystemSettingsRouterService], + }).compile(); - service = module.get(SystemSettingsRouterService); - }); + service = module.get( + SystemSettingsRouterService, + ); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts index 0859f1770..d8112f393 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts index a0fb5beec..e48941003 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { UserRouterService } from './user-router.service'; -describe('UserRouterService', () => { - let service: UserRouterService; +describe('userRouterService', () => { + let service: UserRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [UserRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserRouterService], + }).compile(); - service = module.get(UserRouterService); - }); + service = module.get(UserRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts index 7094ed1fd..177b61bb8 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, userContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts index 28bc6575d..ffb5b963b 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { UserTokensRouterService } from './user-tokens-router.service'; -describe('UserTokensRouterService', () => { - let service: UserTokensRouterService; +describe('userTokensRouterService', () => { + let service: UserTokensRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [UserTokensRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserTokensRouterService], + }).compile(); - service = module.get(UserTokensRouterService); - }); + service = module.get(UserTokensRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts index 8bb1be8e7..578b171b0 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { personalAccessTokenContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { diff --git a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts index 055c9bd4b..0ec3b656c 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ZoneRouterService } from './zone-router.service'; -describe('ZoneRouterService', () => { - let service: ZoneRouterService; +describe('zoneRouterService', () => { + let service: ZoneRouterService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ZoneRouterService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ZoneRouterService], + }).compile(); - service = module.get(ZoneRouterService); - }); + service = module.get(ZoneRouterService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts index c2545e7f8..4575d9745 100644 --- a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts @@ -1,4 +1,4 @@ -import { ServerService } from '@/cpin-module/infrastructure/server/server.service'; +import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; import { AdminAuthorized, zoneContract } from '@cpn-console/shared'; import { Injectable } from '@nestjs/common'; import { @@ -33,10 +33,11 @@ export class ZoneRouterService { const { user, adminPermissions } = await authUser(req); if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403(); - if (!user) + if (!user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } const body = await createZone(data, user.id, req.id); if (body instanceof ErrorResType) return body; @@ -51,10 +52,11 @@ export class ZoneRouterService { const { user, adminPermissions } = await authUser(req); if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403(); - if (!user) + if (!user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } const zoneId = params.zoneId; @@ -71,10 +73,11 @@ export class ZoneRouterService { const { user, adminPermissions } = await authUser(req); if (!AdminAuthorized.isAdmin(adminPermissions)) return new Forbidden403(); - if (!user) + if (!user) { return new Unauthorized401( 'Require to be requested from user not api key', ); + } const zoneId = params.zoneId; const body = await deleteZone(zoneId, user.id, req.id); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.spec.ts index a49109e4c..163446550 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ConfigurationService } from './configuration.service'; -describe('ConfigurationService', () => { - let service: ConfigurationService; +describe('configurationService', () => { + let service: ConfigurationService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ConfigurationService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ConfigurationService], + }).compile(); - service = module.get(ConfigurationService); - }); + service = module.get(ConfigurationService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts index 4423714f5..0e9bf9363 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts @@ -43,5 +43,4 @@ export class ConfigurationService { : process.env.NODE_ENV === 'development' ? 'development' : 'production'; - } diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.spec.ts index b806f3163..49cbd3698 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { DatabaseService } from './database.service'; -describe('DatabaseService', () => { - let service: DatabaseService; +describe('databaseService', () => { + let service: DatabaseService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [DatabaseService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DatabaseService], + }).compile(); - service = module.get(DatabaseService); - }); + service = module.get(DatabaseService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts index 4837dd4bf..29e981c30 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/database/database.service.ts @@ -14,6 +14,7 @@ export class DatabaseService { ? 1000 : 10000; } + DELAY_BEFORE_RETRY!: number; closingConnections = false; diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.spec.ts index 0e6d6797b..0c8a7cd5e 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/http-client/http-client.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { HttpClientService } from './http-client.service'; -describe('HttpClientService', () => { - let service: HttpClientService; +describe('httpClientService', () => { + let service: HttpClientService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [HttpClientService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [HttpClientService], + }).compile(); - service = module.get(HttpClientService); - }); + service = module.get(HttpClientService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts index fcd00fad9..00348b26b 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/infrastructure.module.ts @@ -7,16 +7,8 @@ import { LoggerModule } from './logger/logger.module'; import { ServerService } from './server/server.service'; @Module({ - providers: [ - DatabaseService, - HttpClientService, - ServerService, - ], + providers: [DatabaseService, HttpClientService, ServerService], imports: [LoggerModule, ConfigurationModule], - exports: [ - DatabaseService, - HttpClientService, - ServerService, - ], + exports: [DatabaseService, HttpClientService, ServerService], }) export class InfrastructureModule {} diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts index ba71fe99d..fb8ab018f 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/logger/logger.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { PinoLoggerOptions } from 'fastify/types/logger'; +import type { PinoLoggerOptions } from 'fastify/types/logger'; import { LoggerModule as PinoLoggerModule } from 'nestjs-pino'; import { ConfigurationModule } from '../configuration/configuration.module'; diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.spec.ts b/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.spec.ts index 9df92ef57..ef7ab7463 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.spec.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.spec.ts @@ -1,18 +1,20 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; + import { ServerService } from './server.service'; -describe('ServerService', () => { - let service: ServerService; +describe('serverService', () => { + let service: ServerService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ServerService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ServerService], + }).compile(); - service = module.get(ServerService); - }); + service = module.get(ServerService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.ts b/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.ts index d82590df5..7566c78d3 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/server/server.service.ts @@ -2,12 +2,11 @@ import { Injectable } from '@nestjs/common'; import { initServer } from '@ts-rest/fastify'; @Injectable() -//@TODO is this still necessary ? +// @TODO is this still necessary ? export class ServerService { serverInstance!: any; constructor() { this.serverInstance = initServer(); } - } diff --git a/apps/server-nestjs/src/prisma.ts b/apps/server-nestjs/src/prisma.ts index 4590932b6..4e54f7a77 100644 --- a/apps/server-nestjs/src/prisma.ts +++ b/apps/server-nestjs/src/prisma.ts @@ -1,5 +1,5 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient() +const prisma = new PrismaClient(); -export default prisma +export default prisma; diff --git a/apps/server-nestjs/tsconfig.json b/apps/server-nestjs/tsconfig.json index 33025606b..ac93f0c95 100644 --- a/apps/server-nestjs/tsconfig.json +++ b/apps/server-nestjs/tsconfig.json @@ -15,8 +15,7 @@ "noImplicitAny": false, "outDir": "./dist", "paths": { - "@/*": ["src/*"], - "@old-server/*": ["src/cpin-module/old-server/src/*"] + "@/*": ["src/*"] }, "removeComments": true, "resolvePackageJsonExports": true, From 9a412ef271ba5ccf2a2c2bf6f74bd64e1c490b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 6 Jan 2026 10:30:47 +0100 Subject: [PATCH 32/33] chore(server-nestjs): remove Router module This is a remnant of the previous "framework". It will be implemented through NestJS Controllers aftewards. It was kept as a way to check the viability of the Modularization process. --- .../src/cpin-module/core/app/app.service.ts | 2 - .../src/cpin-module/core/core.module.ts | 3 +- .../admin-role-router.service.spec.ts | 20 -- .../admin-role-router.service.ts | 83 ------ .../admin-token-router.service.spec.ts | 20 -- .../admin-token-router.service.ts | 57 ---- .../cluster-router.service.spec.ts | 20 -- .../cluster-router/cluster-router.service.ts | 162 ----------- .../environment-router.service.spec.ts | 22 -- .../environment-router.service.ts | 157 ----------- .../log-router/log-router.service.spec.ts | 20 -- .../router/log-router/log-router.service.ts | 45 --- .../project-member-router.service.spec.ts | 22 -- .../project-member-router.service.ts | 135 --------- .../project-role-router.service.spec.ts | 22 -- .../project-role-router.service.ts | 139 ---------- .../project-router.service.spec.ts | 20 -- .../project-router/project-router.service.ts | 256 ------------------ .../project-service-router.service.spec.ts | 22 -- .../project-service-router.service.ts | 87 ------ .../repository-router.service.spec.ts | 20 -- .../repository-router.service.ts | 205 -------------- .../cpin-module/core/router/router.module.ts | 52 ---- .../core/router/router.service.spec.ts | 20 -- .../cpin-module/core/router/router.service.ts | 172 ------------ .../service-chain-router.service.spec.ts | 22 -- .../service-chain-router.service.ts | 96 ------- .../service-monitor-router.service.spec.ts | 22 -- .../service-monitor-router.service.ts | 54 ---- .../stage-router/stage-router.service.spec.ts | 20 -- .../stage-router/stage-router.service.ts | 95 ------- .../system-config-router.service.spec.ts | 22 -- .../system-config-router.service.ts | 46 ---- .../system-router.service.spec.ts | 20 -- .../system-router/system-router.service.ts | 30 -- .../system-settings-router.service.spec.ts | 22 -- .../system-settings-router.service.ts | 43 --- .../user-router/user-router.service.spec.ts | 20 -- .../router/user-router/user-router.service.ts | 78 ------ .../user-tokens-router.service.spec.ts | 20 -- .../user-tokens-router.service.ts | 67 ----- .../zone-router/zone-router.service.spec.ts | 20 -- .../router/zone-router/zone-router.service.ts | 93 ------- 43 files changed, 1 insertion(+), 2572 deletions(-) delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/router.module.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts delete mode 100644 apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts diff --git a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts index 5ef1bb83c..b0537ebdd 100644 --- a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts @@ -53,7 +53,6 @@ export class AppService { constructor( private readonly configurationService: ConfigurationService, - private readonly routerService: RouterService, private readonly fastifyService: FastifyService, ) { this.keycloakConf = { @@ -117,7 +116,6 @@ export class AppService { transformObject: () => openApiDocument, }) .register(fastifySwaggerUi, this.fastifyService.swaggerUiConf) - .register(this.routerService.apiRouter()) .addHook('onRoute', (opts) => { if (opts.path === `${apiPrefix}/healthz`) { opts.logLevel = 'silent'; diff --git a/apps/server-nestjs/src/cpin-module/core/core.module.ts b/apps/server-nestjs/src/cpin-module/core/core.module.ts index 595bebd03..a5e6617b0 100644 --- a/apps/server-nestjs/src/cpin-module/core/core.module.ts +++ b/apps/server-nestjs/src/cpin-module/core/core.module.ts @@ -4,10 +4,9 @@ import { ConfigurationModule } from '../infrastructure/configuration/configurati import { InfrastructureModule } from '../infrastructure/infrastructure.module'; import { AppService } from './app/app.service'; import { FastifyService } from './fastify/fastify.service'; -import { RouterModule } from './router/router.module'; @Module({ - imports: [ConfigurationModule, RouterModule, InfrastructureModule], + imports: [ConfigurationModule, InfrastructureModule], providers: [AppService, FastifyService], }) export class CoreModule {} diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts deleted file mode 100644 index f679cb58e..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { AdminRoleRouterService } from './admin-role-router.service'; - -describe('adminRoleRouterService', () => { - let service: AdminRoleRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AdminRoleRouterService], - }).compile(); - - service = module.get(AdminRoleRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts deleted file mode 100644 index b3901e909..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/admin-role-router/admin-role-router.service.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { AdminAuthorized, adminRoleContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from '@old-server/resources/admin-role/business'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class AdminRoleRouterService { - constructor(private readonly serverService: ServerService) {} - - adminRoleRouter() { - return this.serverService.serverInstance.router(adminRoleContract, { - async listAdminRoles() { - const body = await listRoles(); - - return { - status: 200, - body, - }; - }, - - async createAdminRole({ request: req, body }) { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await createRole(body); - - return { - status: 201, - body: resBody, - }; - }, - - async patchAdminRoles({ request: req, body }) { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await patchRoles(body); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - async adminRoleMemberCounts({ request: req }) { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await countRolesMembers(); - - return { - status: 200, - body: resBody, - }; - }, - - async deleteAdminRole({ request: req, params }) { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await deleteRole(params.roleId); - - return { - status: 204, - body: resBody, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts deleted file mode 100644 index f1a3927b4..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { AdminTokenRouterService } from './admin-token-router.service'; - -describe('adminTokenRouterService', () => { - let service: AdminTokenRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AdminTokenRouterService], - }).compile(); - - service = module.get(AdminTokenRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts deleted file mode 100644 index e664d03e8..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/admin-token-router/admin-token-router.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { AdminAuthorized, adminTokenContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - createToken, - deleteToken, - listTokens, -} from '@old-server/resources/admin-token/business'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class AdminTokenRouterService { - constructor(private readonly serverService: ServerService) {} - - adminTokenRouter() { - return this.serverService.serverInstance.router(adminTokenContract, { - listAdminTokens: async ({ request: req, query }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await listTokens(query); - - return { - status: 200, - body, - }; - }, - - createAdminToken: async ({ request: req, body: data }) => { - const perms = await authUser(req); - - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await createToken(data); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - deleteAdminToken: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - await deleteToken(params.tokenId); - - return { - status: 204, - body: null, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts deleted file mode 100644 index dbb18f899..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { ClusterRouterService } from './cluster-router.service'; - -describe('clusterRouterService', () => { - let service: ClusterRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ClusterRouterService], - }).compile(); - - service = module.get(ClusterRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts deleted file mode 100644 index 88b8cda34..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/cluster-router/cluster-router.service.ts +++ /dev/null @@ -1,162 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import type { AsyncReturnType } from '@cpn-console/shared'; -import { AdminAuthorized, clusterContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - createCluster, - deleteCluster, - getClusterAssociatedEnvironments, - getClusterDetails as getClusterDetailsBusiness, - getClusterUsage, - listClusters, - updateCluster, -} from '@old-server/resources/cluster/business'; -import '@old-server/types/index'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - Unauthorized401, -} from '@old-server/utils/errors'; - -@Injectable() -export class ClusterRouterService { - constructor(private readonly serverService: ServerService) {} - clusterRouter() { - return this.serverService.serverInstance.router(clusterContract, { - listClusters: async ({ request: req }) => { - const { adminPermissions, user } = await authUser(req); - - let body: AsyncReturnType = []; - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listClusters(); - } else if (user) { - body = await listClusters(user.id); - } - - return { - status: 200, - body, - }; - }, - - getClusterDetails: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const cluster = await getClusterDetailsBusiness(clusterId); - - return { - status: 200, - body: cluster, - }; - }, - - getClusterUsage: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const usage = await getClusterUsage(clusterId); - - return { - status: 200, - body: usage, - }; - }, - - createCluster: async ({ request: req, body: data }) => { - const { adminPermissions, user } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - - if (!user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - const body = await createCluster(data, user.id, req.id); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - getClusterEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const clusterId = params.clusterId; - const environments = - await getClusterAssociatedEnvironments(clusterId); - - return { - status: 200, - body: environments, - }; - }, - - updateCluster: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - - const clusterId = params.clusterId; - const body = await updateCluster( - data, - clusterId, - user.id, - req.id, - ); - - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - deleteCluster: async ({ - request: req, - params, - query: { force }, - }) => { - const { user, adminPermissions, tokenId } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user?.id && !tokenId) { - return new Unauthorized401( - 'Your identity has not been found', - ); - } - - const clusterId = params.clusterId; - const body = await deleteCluster({ - clusterId, - userId: user?.id, - requestId: req.id, - force, - }); - - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts deleted file mode 100644 index bd5c22e5e..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { EnvironmentRouterService } from './environment-router.service'; - -describe('environmentRouterService', () => { - let service: EnvironmentRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [EnvironmentRouterService], - }).compile(); - - service = module.get( - EnvironmentRouterService, - ); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts deleted file mode 100644 index 59446c561..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/environment-router/environment-router.service.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { ProjectAuthorized, environmentContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - checkEnvironmentCreate, - checkEnvironmentUpdate, - createEnvironment, - deleteEnvironment, - getProjectEnvironments, - updateEnvironment, -} from '@old-server/resources/environment/business'; -import { authUser } from '@old-server/utils/controller'; -import { - BadRequest400, - Forbidden403, - Internal500, - NotFound404, - Unauthorized401, -} from '@old-server/utils/errors'; - -@Injectable() -export class EnvironmentRouterService { - constructor(private readonly serverService: ServerService) {} - - environmentRouter() { - return this.serverService.serverInstance.router(environmentContract, { - listEnvironments: async ({ request: req, query }) => { - const projectId = query.projectId; - const perms = await authUser(req, { id: projectId }); - - const environments = ProjectAuthorized.ListEnvironments(perms) - ? await getProjectEnvironments(projectId) - : []; - - return { - status: 200, - body: environments, - }; - }, - - createEnvironment: async ({ request: req, body: requestBody }) => { - const projectId = requestBody.projectId; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const checkCreateResult = await checkEnvironmentCreate({ - ...requestBody, - }); - if (checkCreateResult.isError) - return new BadRequest400(checkCreateResult.error); - - const result = await createEnvironment({ - userId: perms.user.id, - projectId, - name: requestBody.name, - clusterId: requestBody.clusterId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - stageId: requestBody.stageId, - requestId: req.id, - }); - if (result.isError) { - return new Internal500(result.error); - } - return { - status: 201, - body: result.data, - }; - }, - - updateEnvironment: async ({ - request: req, - body: requestBody, - params, - }) => { - const { environmentId } = params; - const perms = await authUser(req, { environmentId }); - if (!perms.user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - if (!ProjectAuthorized.ListEnvironments(perms)) - return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const checkUpdateResult = await checkEnvironmentUpdate({ - environmentId, - ...requestBody, - }); - if (checkUpdateResult.isError) - return new BadRequest400(checkUpdateResult.error); - - const result = await updateEnvironment({ - user: perms.user, - environmentId, - cpu: requestBody.cpu, - gpu: requestBody.gpu, - memory: requestBody.memory, - requestId: req.id, - }); - if (result.isError) { - return new Internal500(result.error); - } - return { - status: 200, - body: result.data, - }; - }, - - deleteEnvironment: async ({ request: req, params }) => { - const { environmentId } = params; - const perms = await authUser(req, { environmentId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageEnvironments(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const result = await deleteEnvironment({ - userId: perms.user?.id, - environmentId, - requestId: req.id, - projectId: perms.projectId, - }); - if (result.isError) { - return new Internal500(result.error); - } - - return { - status: 204, - body: result.data, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts deleted file mode 100644 index 5d3fe7954..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { LogRouterService } from './log-router.service'; - -describe('logRouterService', () => { - let service: LogRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [LogRouterService], - }).compile(); - - service = module.get(LogRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts deleted file mode 100644 index 5b45f936a..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/log-router/log-router.service.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import type { CleanLog, Log, XOR } from '@cpn-console/shared'; -import { AdminAuthorized, logContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { getLogs } from '@old-server/resources/log/business'; -import type { - UserProfile, - UserProjectProfile, -} from '@old-server/utils/controller'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class LogRouterService { - constructor(private readonly serverService: ServerService) {} - - logRouter() { - return this.serverService.serverInstance.router(logContract, { - // Récupérer des logs - getLogs: async ({ request: req, query }) => { - const perms: XOR = - query.projectId - ? await authUser(req, { id: query.projectId }) - : await authUser(req); - - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) { - if (!perms.projectPermissions) { - return new Forbidden403(); - } - query.clean = true; - } - - const [total, logs] = (await getLogs(query)) as [ - number, - unknown[], - ] as [number, Array]; - - return { - status: 200, - body: { total, logs }, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts deleted file mode 100644 index 9ecff5701..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { ProjectMemberRouterService } from './project-member-router.service'; - -describe('projectMemberRouterService', () => { - let service: ProjectMemberRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectMemberRouterService], - }).compile(); - - service = module.get( - ProjectMemberRouterService, - ); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts deleted file mode 100644 index f1864e486..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/project-member-router/project-member-router.service.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { - AdminAuthorized, - ProjectAuthorized, - projectMemberContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - addMember, - listMembers, - patchMembers, - removeMember, -} from '@old-server/resources/project-member/business'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - NotFound404, - Unauthorized401, -} from '@old-server/utils/errors'; - -@Injectable() -export class ProjectMemberRouterService { - constructor(private readonly serverService: ServerService) {} - - projectMemberRouter() { - return this.serverService.serverInstance.router(projectMemberContract, { - listMembers: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - - const body = await listMembers(projectId); - - return { - status: 200, - body, - }; - }, - - addMember: async ({ request: req, params, body }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - if (!ProjectAuthorized.ManageMembers(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await addMember( - projectId, - body, - perms.user.id, - req.id, - perms.projectOwnerId, - ); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 201, - body: resBody, - }; - }, - - patchMembers: async ({ request: req, params, body }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageMembers(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await patchMembers(projectId, body); - - return { - status: 200, - body: resBody, - }; - }, - - removeMember: async ({ request: req, params }) => { - const { projectId, userId } = params; - const perms = await authUser(req, { id: projectId }); - - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - - if ( - !ProjectAuthorized.ManageMembers(perms) && - userId !== perms.user?.id - ) { - return new Forbidden403(); - } - - const resBody = await removeMember(projectId, params.userId); - - return { - status: 200, - body: resBody, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts deleted file mode 100644 index 7ee22fb44..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { ProjectRoleRouterService } from './project-role-router.service'; - -describe('projectRoleRouterService', () => { - let service: ProjectRoleRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectRoleRouterService], - }).compile(); - - service = module.get( - ProjectRoleRouterService, - ); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts deleted file mode 100644 index 7bc5b7df7..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/project-role-router/project-role-router.service.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { - AdminAuthorized, - ProjectAuthorized, - projectRoleContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - countRolesMembers, - createRole, - deleteRole, - listRoles, - patchRoles, -} from '@old-server/resources/project-role/business'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - NotFound404, -} from '@old-server/utils/errors'; - -@Injectable() -export class ProjectRoleRouterService { - constructor(private readonly serverService: ServerService) {} - - projectRoleRouter() { - return this.serverService.serverInstance.router(projectRoleContract, { - // Récupérer des projets - listProjectRoles: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - - const body = await listRoles(projectId); - - return { - status: 200, - body, - }; - }, - - createProjectRole: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await createRole(projectId, body); - - return { - status: 201, - body: resBody, - }; - }, - - patchProjectRoles: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await patchRoles(projectId, body); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - projectRoleMemberCounts: async ({ request: req, params }) => { - const { projectId } = params; - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - - const resBody = await countRolesMembers(projectId); - - return { - status: 200, - body: resBody, - }; - }, - - deleteProjectRole: async ({ - request: req, - params: { projectId, roleId }, - }) => { - const perms = await authUser(req, { id: projectId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRoles(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const resBody = await deleteRole(roleId); - - return { - status: 204, - body: resBody, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts deleted file mode 100644 index 3378547c2..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { ProjectRouterService } from './project-router.service'; - -describe('projectRouterService', () => { - let service: ProjectRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectRouterService], - }).compile(); - - service = module.get(ProjectRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts deleted file mode 100644 index be1b5d376..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/project-router/project-router.service.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import type { AsyncReturnType } from '@cpn-console/shared'; -import { - AdminAuthorized, - ProjectAuthorized, - projectContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - archiveProject, - bulkActionProject, - createProject, - generateProjectsData, - getProject, - getProjectSecrets, - listProjects, - replayHooks, - updateProject, -} from '@old-server/resources/project/business'; -import { authUser } from '@old-server/utils/controller'; -import { - BadRequest400, - ErrorResType, - Forbidden403, - NotFound404, - Unauthorized401, -} from '@old-server/utils/errors'; - -@Injectable() -export class ProjectRouterService { - constructor(private readonly serverService: ServerService) {} - - projectRouter() { - return this.serverService.serverInstance.router(projectContract, { - // Récupérer des projets - listProjects: async ({ request: req, query }) => { - const { adminPermissions, user } = await authUser(req); - let body: AsyncReturnType = []; - - if (adminPermissions && !user) { - // c'est donc un compte de service - query.filter = 'all'; - } - if ( - query.filter === 'all' && - !AdminAuthorized.isAdmin(adminPermissions) - ) { - return new BadRequest400( - "Seuls les admins avec les droits de visionnage des projets peuvent utiliser le filtre 'all'", - ); - } - - body = await listProjects(query, user?.id); - - return { - status: 200, - body, - }; - }, - - // Récupérer les secrets d'un projet - getProjectSecrets: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.SeeSecrets(perms)) - return new Forbidden403(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await getProjectSecrets(projectId); - - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Créer un projet - createProject: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (perms.user?.type !== 'human') { - return new Unauthorized401( - 'Cannot find requestor in database', - ); - } - const body = await createProject(data, perms.user, req.id); - - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Récuperer un seul projet - getProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.projectId) return new NotFound404(); - if (!isAdmin) { - if (!perms.projectPermissions) { - return new NotFound404(); - } - if (perms.projectStatus === 'archived') { - return new NotFound404(); - } - } - - const body = await getProject(projectId); - - return { - status: 200, - body, - }; - }, - - // Mettre à jour un projet - updateProject: async ({ request: req, params, body: data }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) { - return new Unauthorized401( - 'Cannot find requestor in database', - ); - } - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - const isOwner = perms.projectOwnerId === perms.user.id; - - if (!perms.projectPermissions && !isAdmin) - return new NotFound404(); - if (!isAdmin) { - // filtrage des clés par niveau de permissions - delete data.locked; - if (!isOwner) { - delete data.ownerId; // impossible de toucher à cette clé - } - } - if (perms.projectLocked) { - if (!isAdmin) - return new Forbidden403('Le projet est verrouillé'); - if (data.locked !== false) { - return new Forbidden403( - 'Veuillez déverrouiler le projet pour le mettre à jour', - ); - } - } - - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); - - const body = await updateProject( - data, - projectId, - perms.user, - req.id, - ); - - if (body instanceof ErrorResType) return body; - return { - status: 200, - body, - }; - }, - - // Reprovisionner un projet - replayHooksForProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.projectPermissions && !isAdmin) - return new NotFound404(); - if (!ProjectAuthorized.ReplayHooks(perms)) - return new Forbidden403(); - - const body = await replayHooks({ - projectId, - userId: perms.user?.id, - requestId: req.id, - }); - - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - - // Archiver un projet - archiveProject: async ({ request: req, params }) => { - const projectId = params.projectId; - const perms = await authUser(req, { id: projectId }); - const isAdmin = AdminAuthorized.isAdmin(perms.adminPermissions); - - if (!perms.user) { - return new Unauthorized401( - 'Cannot find requestor in database', - ); - } - if (!perms.projectPermissions && !isAdmin) - return new NotFound404(); - if (!ProjectAuthorized.Manage(perms)) return new Forbidden403(); - - const body = await archiveProject( - projectId, - perms.user, - req.id, - ); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - // Récupérer les données de tous les projets pour export - getProjectsData: async ({ request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - const body = await generateProjectsData(); - - return { - status: 200, - body, - }; - }, - - bulkActionProject: async ({ request: req, body }) => { - const perms = await authUser(req); - - if (!perms.user) { - return new Unauthorized401( - 'Cannot find requestor in database', - ); - } - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - await bulkActionProject(body, perms.user, req.id); - - return { - status: 202, - body: null, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts deleted file mode 100644 index 120837802..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { ProjectServiceRouterService } from './project-service-router.service'; - -describe('projectServiceRouterService', () => { - let service: ProjectServiceRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectServiceRouterService], - }).compile(); - - service = module.get( - ProjectServiceRouterService, - ); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts deleted file mode 100644 index 093555c35..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/project-service-router/project-service-router.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { - AdminAuthorized, - ProjectAuthorized, - projectServiceContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - getProjectServices, - updateProjectServices, -} from '@old-server/resources/project-service/business'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403, NotFound404 } from '@old-server/utils/errors'; - -@Injectable() -export class ProjectServiceRouterService { - constructor(private readonly serverService: ServerService) {} - - projectServiceRouter() { - return this.serverService.serverInstance.router( - projectServiceContract, - { - // Récupérer les services d'un projet - getServices: async ({ - request: req, - params: { projectId }, - query, - }) => { - const perms = await authUser(req, { id: projectId }); - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - if ( - !AdminAuthorized.isAdmin(perms.adminPermissions) && - query.permissionTarget === 'admin' - ) { - return new Forbidden403( - 'Vous ne pouvez pas demander les paramètres admin', - ); - } - - const body = await getProjectServices( - projectId, - query.permissionTarget, - ); - - return { - status: 200, - body, - }; - }, - - updateProjectServices: async ({ - request: req, - params: { projectId }, - body, - }) => { - const perms = await authUser(req, { id: projectId }); - if (!ProjectAuthorized.Manage(perms)) - return new NotFound404(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - - const allowedRoles: Array<'user' | 'admin'> = - AdminAuthorized.isAdmin(perms.adminPermissions) - ? ['user', 'admin'] - : ['user']; - - const resBody = await updateProjectServices( - projectId, - body, - allowedRoles, - ); - return { - status: 204, - body: resBody, - }; - }, - }, - ); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts deleted file mode 100644 index bbc128657..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { RepositoryRouterService } from './repository-router.service'; - -describe('repositoryRouterService', () => { - let service: RepositoryRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [RepositoryRouterService], - }).compile(); - - service = module.get(RepositoryRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts deleted file mode 100644 index a703868e6..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/repository-router/repository-router.service.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { - AdminAuthorized, - ProjectAuthorized, - fakeToken, - repositoryContract, -} from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - createRepository, - deleteRepository, - getProjectRepositories, - syncRepository, - updateRepository, -} from '@old-server/resources/repository/business'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - NotFound404, - Unauthorized401, -} from '@old-server/utils/errors'; -import { filterObjectByKeys } from '@old-server/utils/queries-tools'; - -@Injectable() -export class RepositoryRouterService { - constructor(private readonly serverService: ServerService) {} - - repositoryRouter() { - return this.serverService.serverInstance.router(repositoryContract, { - // Récupérer tous les repositories d'un projet - listRepositories: async ({ request: req, query }) => { - const projectId = query.projectId; - const perms = await authUser(req, { id: projectId }); - - const body = ProjectAuthorized.ListRepositories(perms) - ? await getProjectRepositories(projectId) - : []; - - return { - status: 200, - body, - }; - }, - - // Synchroniser un repository - syncRepository: async ({ request: req, params, body }) => { - const { repositoryId } = params; - const perms = await authUser(req, { repositoryId }); - if (!perms.user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const { syncAllBranches, branchName } = body; - - const resBody = await syncRepository({ - repositoryId, - userId: perms.user.id, - branchName, - requestId: req.id, - syncAllBranches, - }); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 204, - body: resBody, - }; - }, - - // Créer un repository - createRepository: async ({ request: req, body: data }) => { - const projectId = data.projectId; - const perms = await authUser(req, { id: projectId }); - - if (!perms.user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await createRepository({ - data, - userId: perms.user.id, - requestId: req.id, - }); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Mettre à jour un repository - updateRepository: async ({ request: req, params, body }) => { - const repositoryId = params.repositoryId; - const perms = await authUser(req, { repositoryId }); - - if (!perms.user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - if ( - !perms.projectPermissions && - !AdminAuthorized.isAdmin(perms.adminPermissions) - ) { - return new NotFound404(); - } - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const keysAllowedForUpdate = [ - 'externalRepoUrl', - 'isPrivate', - 'externalToken', - 'externalUserName', - 'isInfra', - 'deployRevision', - 'deployPath', - 'helmValuesFiles', - ]; - const data = filterObjectByKeys(body, keysAllowedForUpdate); - - if (data.externalToken === fakeToken) { - delete data.externalToken; - } - - if (data.isPrivate === false) { - delete data.externalToken; - delete data.externalUserName; - } - - const resBody = await updateRepository({ - repositoryId, - data, - userId: perms.user.id, - requestId: req.id, - }); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 200, - body: resBody, - }; - }, - - // Supprimer un repository - deleteRepository: async ({ request: req, params }) => { - const repositoryId = params.repositoryId; - const perms = await authUser(req, { repositoryId }); - - if (!perms.user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - if (!perms.projectPermissions) return new NotFound404(); - if (!ProjectAuthorized.ManageRepositories(perms)) - return new Forbidden403(); - if (perms.projectLocked) - return new Forbidden403('Le projet est verrouillé'); - if (perms.projectStatus === 'archived') - return new Forbidden403('Le projet est archivé'); - - const body = await deleteRepository({ - repositoryId, - userId: perms.user.id, - requestId: req.id, - projectId: perms.projectId, - }); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/router.module.ts b/apps/server-nestjs/src/cpin-module/core/router/router.module.ts deleted file mode 100644 index c5e0f9bf7..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/router.module.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ConfigurationModule } from '@/cpin-module/infrastructure/configuration/configuration.module'; -import { InfrastructureModule } from '@/cpin-module/infrastructure/infrastructure.module'; -import { Module } from '@nestjs/common'; - -import { AdminRoleRouterService } from './admin-role-router/admin-role-router.service'; -import { AdminTokenRouterService } from './admin-token-router/admin-token-router.service'; -import { ClusterRouterService } from './cluster-router/cluster-router.service'; -import { EnvironmentRouterService } from './environment-router/environment-router.service'; -import { LogRouterService } from './log-router/log-router.service'; -import { ProjectMemberRouterService } from './project-member-router/project-member-router.service'; -import { ProjectRoleRouterService } from './project-role-router/project-role-router.service'; -import { ProjectRouterService } from './project-router/project-router.service'; -import { ProjectServiceRouterService } from './project-service-router/project-service-router.service'; -import { RepositoryRouterService } from './repository-router/repository-router.service'; -import { RouterService } from './router.service'; -import { ServiceChainRouterService } from './service-chain-router/service-chain-router.service'; -import { ServiceMonitorRouterService } from './service-monitor-router/service-monitor-router.service'; -import { StageRouterService } from './stage-router/stage-router.service'; -import { SystemConfigRouterService } from './system-config-router/system-config-router.service'; -import { SystemRouterService } from './system-router/system-router.service'; -import { SystemSettingsRouterService } from './system-settings-router/system-settings-router.service'; -import { UserRouterService } from './user-router/user-router.service'; -import { UserTokensRouterService } from './user-tokens-router/user-tokens-router.service'; -import { ZoneRouterService } from './zone-router/zone-router.service'; - -@Module({ - imports: [InfrastructureModule, ConfigurationModule], - providers: [ - AdminRoleRouterService, - AdminTokenRouterService, - ClusterRouterService, - EnvironmentRouterService, - LogRouterService, - ProjectMemberRouterService, - ProjectRoleRouterService, - ProjectRouterService, - ProjectServiceRouterService, - RepositoryRouterService, - RouterService, - ServiceChainRouterService, - ServiceMonitorRouterService, - StageRouterService, - SystemConfigRouterService, - SystemRouterService, - SystemSettingsRouterService, - UserRouterService, - UserTokensRouterService, - ZoneRouterService, - ], - exports: [RouterService], -}) -export class RouterModule {} diff --git a/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts deleted file mode 100644 index 8311db3ba..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { RouterService } from './router.service'; - -describe('routerService', () => { - let service: RouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [RouterService], - }).compile(); - - service = module.get(RouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/router.service.ts deleted file mode 100644 index 6204668d9..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/router.service.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { Injectable } from '@nestjs/common'; -import type { FastifyInstance } from 'fastify'; - -import type { AdminRoleRouterService } from './admin-role-router/admin-role-router.service'; -import type { AdminTokenRouterService } from './admin-token-router/admin-token-router.service'; -import type { ClusterRouterService } from './cluster-router/cluster-router.service'; -import type { EnvironmentRouterService } from './environment-router/environment-router.service'; -import type { LogRouterService } from './log-router/log-router.service'; -import type { ProjectMemberRouterService } from './project-member-router/project-member-router.service'; -import type { ProjectRoleRouterService } from './project-role-router/project-role-router.service'; -import type { ProjectRouterService } from './project-router/project-router.service'; -import type { ProjectServiceRouterService } from './project-service-router/project-service-router.service'; -import type { RepositoryRouterService } from './repository-router/repository-router.service'; -import type { ServiceChainRouterService } from './service-chain-router/service-chain-router.service'; -import type { ServiceMonitorRouterService } from './service-monitor-router/service-monitor-router.service'; -import type { StageRouterService } from './stage-router/stage-router.service'; -import type { SystemConfigRouterService } from './system-config-router/system-config-router.service'; -import type { SystemRouterService } from './system-router/system-router.service'; -import type { SystemSettingsRouterService } from './system-settings-router/system-settings-router.service'; -import type { UserRouterService } from './user-router/user-router.service'; -import type { UserTokensRouterService } from './user-tokens-router/user-tokens-router.service'; -import type { ZoneRouterService } from './zone-router/zone-router.service'; - -@Injectable() -export class RouterService { - constructor( - private readonly serverService: ServerService, - private readonly adminRoleRouterService: AdminRoleRouterService, - private readonly adminTokenRouterService: AdminTokenRouterService, - private readonly clusterRouterService: ClusterRouterService, - private readonly environmentRouterService: EnvironmentRouterService, - private readonly logRouterService: LogRouterService, - private readonly projectMemberRouterService: ProjectMemberRouterService, - private readonly projectRoleRouterService: ProjectRoleRouterService, - private readonly projectRouterService: ProjectRouterService, - private readonly projectServiceRouterService: ProjectServiceRouterService, - private readonly repositoryRouterService: RepositoryRouterService, - private readonly serviceChainRouterService: ServiceChainRouterService, - private readonly serviceMonitorRouterService: ServiceMonitorRouterService, - private readonly stageRouterService: StageRouterService, - private readonly systemConfigRouterService: SystemConfigRouterService, - private readonly systemRouterService: SystemRouterService, - private readonly systemSettingsRouterService: SystemSettingsRouterService, - private readonly userRouterService: UserRouterService, - private readonly userTokensRouterService: UserTokensRouterService, - private readonly zoneRouterService: ZoneRouterService, - ) {} - - // relax validation schema if NO_VALIDATION env var is set to true. - // /!\ It can lead to security leaks !!!! - validateTrue = { responseValidation: process.env.NO_VALIDATION !== 'true' }; - - apiRouter() { - return async (app: FastifyInstance) => { - await app.register( - this.serverService.serverInstance.plugin( - this.adminRoleRouterService.adminRoleRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.adminTokenRouterService.adminTokenRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.clusterRouterService.clusterRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.serviceChainRouterService.serviceChainRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.environmentRouterService.environmentRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.logRouterService.logRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.userTokensRouterService.userTokensRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.projectRouterService.projectRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.projectMemberRouterService.projectMemberRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.projectRoleRouterService.projectRoleRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.projectServiceRouterService.projectServiceRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.repositoryRouterService.repositoryRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.serviceMonitorRouterService.serviceMonitorRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.systemConfigRouterService.systemConfigRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.stageRouterService.stageRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.systemRouterService.systemRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.systemSettingsRouterService.systemSettingsRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.userRouterService.userRouter(), - ), - this.validateTrue, - ); - await app.register( - this.serverService.serverInstance.plugin( - this.zoneRouterService.zoneRouter(), - ), - this.validateTrue, - ); - }; - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts deleted file mode 100644 index 096ae0428..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { ServiceChainRouterService } from './service-chain-router.service'; - -describe('serviceChainRouterService', () => { - let service: ServiceChainRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ServiceChainRouterService], - }).compile(); - - service = module.get( - ServiceChainRouterService, - ); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts deleted file mode 100644 index fe3f4692f..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/service-chain-router/service-chain-router.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import type { AsyncReturnType } from '@cpn-console/shared'; -import { AdminAuthorized, serviceChainContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - getServiceChainDetails as getServiceChainDetailsBusiness, - getServiceChainFlows as getServiceChainFlowsBusiness, - listServiceChains as listServiceChainsBusiness, - retryServiceChain as retryServiceChainBusiness, - validateServiceChain as validateServiceChainBusiness, -} from '@old-server/resources/service-chain/business'; -import '@old-server/types/index'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class ServiceChainRouterService { - constructor(private readonly serverService: ServerService) {} - - serviceChainRouter() { - return this.serverService.serverInstance.router(serviceChainContract, { - listServiceChains: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - - let body: AsyncReturnType = - []; - if (AdminAuthorized.isAdmin(adminPermissions)) { - body = await listServiceChainsBusiness(); - } - - return { - status: 200, - body, - }; - }, - - getServiceChainDetails: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - const serviceChainDetails = - await getServiceChainDetailsBusiness(serviceChainId); - - return { - status: 200, - body: serviceChainDetails, - }; - }, - - retryServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - await retryServiceChainBusiness(serviceChainId); - - return { - status: 204, - body: null, - }; - }, - - validateServiceChain: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.validationId; - await validateServiceChainBusiness(serviceChainId); - - return { - status: 204, - body: null, - }; - }, - - getServiceChainFlows: async ({ params, request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const serviceChainId = params.serviceChainId; - const serviceChainFlows = - await getServiceChainFlowsBusiness(serviceChainId); - - return { - status: 200, - body: serviceChainFlows, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts deleted file mode 100644 index 28afbe393..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { ServiceMonitorRouterService } from './service-monitor-router.service'; - -describe('serviceMonitorRouterService', () => { - let service: ServiceMonitorRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ServiceMonitorRouterService], - }).compile(); - - service = module.get( - ServiceMonitorRouterService, - ); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts deleted file mode 100644 index b1a4ebeb1..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/service-monitor-router/service-monitor-router.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { AdminAuthorized, serviceContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - checkServicesHealth, - refreshServicesHealth, -} from '@old-server/resources/service-monitor/business'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class ServiceMonitorRouterService { - constructor(private readonly serverService: ServerService) {} - - serviceMonitorRouter() { - return this.serverService.serverInstance.router(serviceContract, { - getServiceHealth: async () => { - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - - getCompleteServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - - refreshServiceHealth: async ({ request: req }) => { - const { adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - - await refreshServicesHealth(); - const serviceData = checkServicesHealth(); - - return { - status: 200, - body: serviceData, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts deleted file mode 100644 index 2c4fbf095..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { StageRouterService } from './stage-router.service'; - -describe('stageRouterService', () => { - let service: StageRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [StageRouterService], - }).compile(); - - service = module.get(StageRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts deleted file mode 100644 index 086056723..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/stage-router/stage-router.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { AdminAuthorized, stageContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - createStage, - deleteStage, - getStageAssociatedEnvironments, - listStages, - updateStage, -} from '@old-server/resources/stage/business'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class StageRouterService { - constructor(private readonly serverService: ServerService) {} - stageRouter() { - return this.serverService.serverInstance.router(stageContract, { - // Récupérer les types d'environnement disponibles - listStages: async () => { - const body = await listStages(); - - return { - status: 200, - body, - }; - }, - - // Récupérer les environnements associés au stage - getStageEnvironments: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - const body = await getStageAssociatedEnvironments(stageId); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Créer un stage - createStage: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const body = await createStage(data); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - // Modifier une association stage / clusters - updateStage: async ({ request: req, params, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - - const body = await updateStage(stageId, data); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - // Supprimer un stage - deleteStage: async ({ request: req, params }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const stageId = params.stageId; - - const body = await deleteStage(stageId); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts deleted file mode 100644 index 976c92f68..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { SystemConfigRouterService } from './system-config-router.service'; - -describe('systemConfigRouterService', () => { - let service: SystemConfigRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [SystemConfigRouterService], - }).compile(); - - service = module.get( - SystemConfigRouterService, - ); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts deleted file mode 100644 index 387355e6f..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/system-config-router/system-config-router.service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { AdminAuthorized, systemPluginContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - getPluginsConfig, - updatePluginConfig, -} from '@old-server/resources/system/config/business'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class SystemConfigRouterService { - constructor(private readonly serverService: ServerService) {} - - systemConfigRouter() { - return this.serverService.serverInstance.router(systemPluginContract, { - // Récupérer les configurations plugins - getPluginsConfig: async ({ request: req }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const services = await getPluginsConfig(); - - return { - status: 200, - body: services, - }; - }, - // Mettre à jour les configurations plugins - updatePluginsConfig: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const resBody = await updatePluginConfig(body); - if (resBody instanceof ErrorResType) return resBody; - - return { - status: 204, - body: resBody, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts deleted file mode 100644 index d35f07f6c..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { SystemRouterService } from './system-router.service'; - -describe('systemRouterService', () => { - let service: SystemRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [SystemRouterService], - }).compile(); - - service = module.get(SystemRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts deleted file mode 100644 index 483c2f300..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/system-router/system-router.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'; -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { systemContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class SystemRouterService { - constructor( - private readonly configurationService: ConfigurationService, - private readonly serverService: ServerService, - ) {} - - systemRouter() { - return this.serverService.serverInstance.router(systemContract, { - getVersion: async () => ({ - status: 200, - body: { - version: this.configurationService.appVersion, - }, - }), - - getHealth: async () => ({ - status: 200, - body: { - status: 'OK', - }, - }), - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts deleted file mode 100644 index 1fe094e4d..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { SystemSettingsRouterService } from './system-settings-router.service'; - -describe('systemSettingsRouterService', () => { - let service: SystemSettingsRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [SystemSettingsRouterService], - }).compile(); - - service = module.get( - SystemSettingsRouterService, - ); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts deleted file mode 100644 index d8112f393..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/system-settings-router/system-settings-router.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { AdminAuthorized, systemSettingsContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - getSystemSettings, - upsertSystemSetting, -} from '@old-server/resources/system/settings/business'; -import { authUser } from '@old-server/utils/controller'; -import { Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class SystemSettingsRouterService { - constructor(private readonly serverService: ServerService) {} - - systemSettingsRouter() { - return this.serverService.serverInstance.router( - systemSettingsContract, - { - listSystemSettings: async ({ query }) => { - const systemSettings = await getSystemSettings(query.key); - - return { - status: 200, - body: systemSettings, - }; - }, - - upsertSystemSetting: async ({ request: req, body: data }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const systemSetting = await upsertSystemSetting(data); - - return { - status: 201, - body: systemSetting, - }; - }, - }, - ); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts deleted file mode 100644 index e48941003..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { UserRouterService } from './user-router.service'; - -describe('userRouterService', () => { - let service: UserRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [UserRouterService], - }).compile(); - - service = module.get(UserRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts deleted file mode 100644 index 177b61bb8..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/user-router/user-router.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { AdminAuthorized, userContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - getMatchingUsers, - getUsers, - logViaSession, - patchUsers, -} from '@old-server/resources/user/business'; -import '@old-server/types/index'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - Unauthorized401, -} from '@old-server/utils/errors'; - -@Injectable() -export class UserRouterService { - constructor(private readonly serverService: ServerService) {} - - userRouter() { - return this.serverService.serverInstance.router(userContract, { - getMatchingUsers: async ({ query }) => { - const usersMatching = await getMatchingUsers(query); - - return { - status: 200, - body: usersMatching, - }; - }, - - auth: async ({ request: req }) => { - const user = req.session.user; - - if (!user) return new Unauthorized401(); - - const { user: body } = await logViaSession(user); - - return { - status: 200, - body, - }; - }, - - getAllUsers: async ({ - request: req, - query: { relationType, ...query }, - }) => { - const perms = await authUser(req); - - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const body = await getUsers(query, relationType); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - patchUsers: async ({ request: req, body }) => { - const perms = await authUser(req); - if (!AdminAuthorized.isAdmin(perms.adminPermissions)) - return new Forbidden403(); - - const users = await patchUsers(body); - - return { - status: 200, - body: users, - }; - }, - }); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts deleted file mode 100644 index ffb5b963b..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { UserTokensRouterService } from './user-tokens-router.service'; - -describe('userTokensRouterService', () => { - let service: UserTokensRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [UserTokensRouterService], - }).compile(); - - service = module.get(UserTokensRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts deleted file mode 100644 index 578b171b0..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/user-tokens-router/user-tokens-router.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { personalAccessTokenContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - createToken, - deleteToken, - listTokens, -} from '@old-server/resources/user/tokens/business'; -// @TODO: Nécessaire ? -// import '@old-server/types/index'; -import { authUser } from '@old-server/utils/controller'; -import { ErrorResType, Forbidden403 } from '@old-server/utils/errors'; - -@Injectable() -export class UserTokensRouterService { - constructor(private readonly serverService: ServerService) {} - - userTokensRouter() { - return this.serverService.serverInstance.router( - personalAccessTokenContract, - { - listPersonalAccessTokens: async ({ request: req }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - const body = await listTokens(perms.user.id); - - return { - status: 200, - body, - }; - }, - - createPersonalAccessToken: async ({ - request: req, - body: data, - }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - const body = await createToken(data, perms.user.id); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - deletePersonalAccessToken: async ({ request: req, params }) => { - const perms = await authUser(req); - - if (!perms.user?.id || perms.user?.type !== 'human') - return new Forbidden403(); - await deleteToken(params.tokenId, perms.user.id); - - return { - status: 204, - body: null, - }; - }, - }, - ); - } -} diff --git a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts deleted file mode 100644 index 0ec3b656c..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; - -import { ZoneRouterService } from './zone-router.service'; - -describe('zoneRouterService', () => { - let service: ZoneRouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ZoneRouterService], - }).compile(); - - service = module.get(ZoneRouterService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts b/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts deleted file mode 100644 index 4575d9745..000000000 --- a/apps/server-nestjs/src/cpin-module/core/router/zone-router/zone-router.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { ServerService } from '@/cpin-module/infrastructure/server/server.service'; -import { AdminAuthorized, zoneContract } from '@cpn-console/shared'; -import { Injectable } from '@nestjs/common'; -import { - createZone, - deleteZone, - listZones, - updateZone, -} from '@old-server/resources/zone/business'; -import { authUser } from '@old-server/utils/controller'; -import { - ErrorResType, - Forbidden403, - Unauthorized401, -} from '@old-server/utils/errors'; - -@Injectable() -export class ZoneRouterService { - constructor(private readonly serverService: ServerService) {} - - zoneRouter() { - return this.serverService.serverInstance.router(zoneContract, { - listZones: async () => { - const zones = await listZones(); - - return { - status: 200, - body: zones, - }; - }, - - createZone: async ({ request: req, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - - const body = await createZone(data, user.id, req.id); - if (body instanceof ErrorResType) return body; - - return { - status: 201, - body, - }; - }, - - updateZone: async ({ request: req, params, body: data }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - - const zoneId = params.zoneId; - - const body = await updateZone(zoneId, data, user.id, req.id); - if (body instanceof ErrorResType) return body; - - return { - status: 200, - body, - }; - }, - - deleteZone: async ({ request: req, params }) => { - const { user, adminPermissions } = await authUser(req); - if (!AdminAuthorized.isAdmin(adminPermissions)) - return new Forbidden403(); - if (!user) { - return new Unauthorized401( - 'Require to be requested from user not api key', - ); - } - const zoneId = params.zoneId; - - const body = await deleteZone(zoneId, user.id, req.id); - if (body instanceof ErrorResType) return body; - - return { - status: 204, - body, - }; - }, - }); - } -} From 771768c244320858bfec89c7963f07c7ff936f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 6 Jan 2026 10:35:45 +0100 Subject: [PATCH 33/33] chore(server-nestjs): fix a fastify type issue --- .../src/cpin-module/core/app/app.service.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts index b0537ebdd..7f5fa6b61 100644 --- a/apps/server-nestjs/src/cpin-module/core/app/app.service.ts +++ b/apps/server-nestjs/src/cpin-module/core/app/app.service.ts @@ -9,7 +9,10 @@ import { } from '@cpn-console/shared'; import fastifyCookie from '@fastify/cookie'; import helmet from '@fastify/helmet'; -import type { FastifySessionOptions } from '@fastify/session'; +import type { + FastifySessionObject, + FastifySessionOptions, +} from '@fastify/session'; import fastifySession from '@fastify/session'; import fastifySwagger from '@fastify/swagger'; import fastifySwaggerUi from '@fastify/swagger-ui'; @@ -30,6 +33,10 @@ interface KeycloakPayload { groups: string[]; } +interface FastifySessionObjectWithUser extends FastifySessionObject { + user: { id: string }; +} + function userPayloadMapper(userPayload: KeycloakPayload) { return { id: userPayload.sub, @@ -136,17 +143,29 @@ export class AppService { if (res.statusCode < 400) { req.log.info({ status: res.statusCode, - userId: req.session?.user?.id, + userId: ( + req.session as + | FastifySessionObjectWithUser + | undefined + )?.user?.id, }); } else if (res.statusCode < 500) { req.log.warn({ status: res.statusCode, - userId: req.session?.user?.id, + userId: ( + req.session as + | FastifySessionObjectWithUser + | undefined + )?.user?.id, }); } else { req.log.error({ status: res.statusCode, - userId: req.session?.user?.id, + userId: ( + req.session as + | FastifySessionObjectWithUser + | undefined + )?.user?.id, }); } });